mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
feat: 历史记录页面 添加本地和云端两种记录支持,支持歌曲、歌单、专辑
This commit is contained in:
@@ -40,7 +40,8 @@ export default defineConfig({
|
||||
],
|
||||
publicDir: resolve('resources'),
|
||||
server: {
|
||||
host: '0.0.0.0'
|
||||
host: '0.0.0.0',
|
||||
port: 2389
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
export default {
|
||||
title: 'Play History',
|
||||
playCount: '{count}',
|
||||
getHistoryFailed: 'Failed to get play history'
|
||||
getHistoryFailed: 'Failed to get play history',
|
||||
categoryTabs: {
|
||||
songs: 'Songs',
|
||||
playlists: 'Playlists',
|
||||
albums: 'Albums'
|
||||
},
|
||||
tabs: {
|
||||
all: 'All Records',
|
||||
local: 'Local Records',
|
||||
cloud: 'Cloud Records'
|
||||
},
|
||||
getCloudRecordFailed: 'Failed to get cloud records',
|
||||
needLogin: 'Please login with cookie to view cloud records',
|
||||
merging: 'Merging records...',
|
||||
noDescription: 'No description',
|
||||
noData: 'No records',
|
||||
newKey: 'New translation'
|
||||
};
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
export default {
|
||||
title: '再生履歴',
|
||||
playCount: '{count}',
|
||||
getHistoryFailed: '履歴の取得に失敗しました'
|
||||
getHistoryFailed: '履歴の取得に失敗しました',
|
||||
tabs: {
|
||||
all: 'すべての記録',
|
||||
local: 'ローカル記録',
|
||||
cloud: 'クラウド記録'
|
||||
},
|
||||
getCloudRecordFailed: 'クラウド記録の取得に失敗しました',
|
||||
needLogin: 'cookieを使用してログインしてクラウド記録を表示できます',
|
||||
merging: '記録を統合中...'
|
||||
};
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
export default {
|
||||
title: '재생 기록',
|
||||
playCount: '{count}',
|
||||
getHistoryFailed: '기록 가져오기 실패'
|
||||
getHistoryFailed: '기록 가져오기 실패',
|
||||
tabs: {
|
||||
all: '전체 기록',
|
||||
local: '로컬 기록',
|
||||
cloud: '클라우드 기록'
|
||||
},
|
||||
getCloudRecordFailed: '클라우드 기록 가져오기 실패',
|
||||
needLogin: 'cookie를 사용하여 로그인하여 클라우드 기록을 볼 수 있습니다',
|
||||
merging: '기록 병합 중...'
|
||||
};
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
export default {
|
||||
title: '播放历史',
|
||||
playCount: '{count}',
|
||||
getHistoryFailed: '获取历史记录失败'
|
||||
getHistoryFailed: '获取历史记录失败',
|
||||
categoryTabs: {
|
||||
songs: '歌曲',
|
||||
playlists: '歌单',
|
||||
albums: '专辑'
|
||||
},
|
||||
tabs: {
|
||||
all: '全部记录',
|
||||
local: '本地记录',
|
||||
cloud: '云端记录'
|
||||
},
|
||||
getCloudRecordFailed: '获取云端记录失败',
|
||||
needLogin: '请使用cookie登录以查看云端记录',
|
||||
merging: '正在合并记录...',
|
||||
noDescription: '暂无描述',
|
||||
noData: '暂无记录'
|
||||
};
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
export default {
|
||||
title: '播放歷史',
|
||||
playCount: '{count}',
|
||||
getHistoryFailed: '取得歷史記錄失敗'
|
||||
getHistoryFailed: '取得歷史記錄失敗',
|
||||
categoryTabs: {
|
||||
songs: '歌曲',
|
||||
playlists: '歌單',
|
||||
albums: '專輯'
|
||||
},
|
||||
tabs: {
|
||||
all: '全部記錄',
|
||||
local: '本地記錄',
|
||||
cloud: '雲端記錄'
|
||||
},
|
||||
getCloudRecordFailed: '取得雲端記錄失敗',
|
||||
needLogin: '請使用cookie登入以查看雲端記錄',
|
||||
merging: '正在合併記錄...',
|
||||
noDescription: '暫無描述',
|
||||
noData: '暫無記錄'
|
||||
};
|
||||
|
||||
@@ -21,7 +21,9 @@ import { useRouter } from 'vue-router';
|
||||
import TrafficWarningDrawer from '@/components/TrafficWarningDrawer.vue';
|
||||
import { usePlayerStore } from '@/store/modules/player';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { isElectron, isLyricWindow } from '@/utils';
|
||||
import { checkLoginStatus } from '@/utils/auth';
|
||||
|
||||
import { initAudioListeners, initMusicHook } from './hooks/MusicHook';
|
||||
import { audioService } from './services/audioService';
|
||||
@@ -31,6 +33,7 @@ import { useAppShortcuts } from './utils/appShortcuts';
|
||||
const { locale } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
const playerStore = usePlayerStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
// 监听语言变化
|
||||
@@ -72,6 +75,17 @@ if (!isLyricWindow.value) {
|
||||
settingsStore.initializeSettings();
|
||||
settingsStore.initializeTheme();
|
||||
settingsStore.initializeSystemFonts();
|
||||
|
||||
// 初始化登录状态 - 从 localStorage 恢复用户信息和登录类型
|
||||
const loginInfo = checkLoginStatus();
|
||||
if (loginInfo.isLoggedIn) {
|
||||
if (loginInfo.user && !userStore.user) {
|
||||
userStore.setUser(loginInfo.user);
|
||||
}
|
||||
if (loginInfo.loginType && !userStore.loginType) {
|
||||
userStore.setLoginType(loginInfo.loginType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSetLanguage(settingsStore.setData.language);
|
||||
|
||||
@@ -20,6 +20,33 @@ export function getUserRecord(uid: number, type: number = 0) {
|
||||
} as any);
|
||||
}
|
||||
|
||||
// 最近播放-歌曲
|
||||
// /record/recent/song
|
||||
export function getRecentSongs(limit: number = 100) {
|
||||
return request.get('/record/recent/song', {
|
||||
params: { limit },
|
||||
noRetry: true
|
||||
} as any);
|
||||
}
|
||||
|
||||
// 最近播放-歌单
|
||||
// /record/recent/playlist
|
||||
export function getRecentPlaylists(limit: number = 100) {
|
||||
return request.get('/record/recent/playlist', {
|
||||
params: { limit },
|
||||
noRetry: true
|
||||
} as any);
|
||||
}
|
||||
|
||||
// 最近播放-专辑
|
||||
// /record/recent/album
|
||||
export function getRecentAlbums(limit: number = 100) {
|
||||
return request.get('/record/recent/album', {
|
||||
params: { limit },
|
||||
noRetry: true
|
||||
} as any);
|
||||
}
|
||||
|
||||
// 获取用户关注列表
|
||||
// /user/follows?uid=32953014
|
||||
export function getUserFollows(uid: number, limit: number = 30, offset: number = 0) {
|
||||
|
||||
112
src/renderer/components/common/AlbumItem.vue
Normal file
112
src/renderer/components/common/AlbumItem.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="album-item" @click="handleClick">
|
||||
<n-image
|
||||
:src="getImgUrl(item.picUrl || '', '100y100')"
|
||||
class="album-item-img"
|
||||
lazy
|
||||
preview-disabled
|
||||
/>
|
||||
<div class="album-item-info">
|
||||
<div class="album-item-name">
|
||||
<n-ellipsis :line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||
</div>
|
||||
<div class="album-item-desc">
|
||||
{{ getDescription() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showCount && item.count" class="album-item-count">
|
||||
{{ item.count }}
|
||||
</div>
|
||||
<div v-if="showDelete" class="album-item-delete" @click.stop="handleDelete">
|
||||
<i class="iconfont icon-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import type { AlbumHistoryItem } from '@/hooks/AlbumHistoryHook';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
item: AlbumHistoryItem;
|
||||
showCount?: boolean;
|
||||
showDelete?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showCount: false,
|
||||
showDelete: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [item: AlbumHistoryItem];
|
||||
delete: [item: AlbumHistoryItem];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const getDescription = () => {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (props.item.artist?.name) {
|
||||
parts.push(props.item.artist.name);
|
||||
}
|
||||
|
||||
if (props.item.size !== undefined) {
|
||||
parts.push(t('user.album.songCount', { count: props.item.size }));
|
||||
}
|
||||
|
||||
return parts.join(' · ') || t('history.noDescription');
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click', props.item);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
emit('delete', props.item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.album-item {
|
||||
@apply flex items-center px-2 py-2 rounded-xl cursor-pointer;
|
||||
@apply transition-all duration-200;
|
||||
@apply bg-light-100 dark:bg-dark-100;
|
||||
@apply hover:bg-light-200 dark:hover:bg-dark-200;
|
||||
@apply mb-2;
|
||||
|
||||
&-img {
|
||||
@apply flex items-center justify-center rounded-xl;
|
||||
@apply w-[60px] h-[60px] flex-shrink-0;
|
||||
@apply bg-light-300 dark:bg-dark-300;
|
||||
}
|
||||
|
||||
&-info {
|
||||
@apply ml-3 flex-1 min-w-0;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply text-gray-900 dark:text-white text-base mb-1;
|
||||
}
|
||||
|
||||
&-desc {
|
||||
@apply text-sm text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-count {
|
||||
@apply px-4 text-lg text-center min-w-[60px];
|
||||
@apply text-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-delete {
|
||||
@apply cursor-pointer rounded-full border-2 w-8 h-8 flex justify-center items-center;
|
||||
@apply border-gray-400 dark:border-gray-600;
|
||||
@apply text-gray-600 dark:text-gray-400;
|
||||
@apply hover:border-red-500 hover:text-red-500;
|
||||
@apply transition-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
112
src/renderer/components/common/PlaylistItem.vue
Normal file
112
src/renderer/components/common/PlaylistItem.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="playlist-item" @click="handleClick">
|
||||
<n-image
|
||||
:src="getImgUrl(item.coverImgUrl || item.picUrl || '', '100y100')"
|
||||
class="playlist-item-img"
|
||||
lazy
|
||||
preview-disabled
|
||||
/>
|
||||
<div class="playlist-item-info">
|
||||
<div class="playlist-item-name">
|
||||
<n-ellipsis :line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||
</div>
|
||||
<div class="playlist-item-desc">
|
||||
{{ getDescription() }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showCount && item.count" class="playlist-item-count">
|
||||
{{ item.count }}
|
||||
</div>
|
||||
<div v-if="showDelete" class="playlist-item-delete" @click.stop="handleDelete">
|
||||
<i class="iconfont icon-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import type { PlaylistHistoryItem } from '@/hooks/PlaylistHistoryHook';
|
||||
import { getImgUrl } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
item: PlaylistHistoryItem;
|
||||
showCount?: boolean;
|
||||
showDelete?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
showCount: false,
|
||||
showDelete: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [item: PlaylistHistoryItem];
|
||||
delete: [item: PlaylistHistoryItem];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const getDescription = () => {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (props.item.trackCount !== undefined) {
|
||||
parts.push(t('user.playlist.trackCount', { count: props.item.trackCount }));
|
||||
}
|
||||
|
||||
if (props.item.creator?.nickname) {
|
||||
parts.push(props.item.creator.nickname);
|
||||
}
|
||||
|
||||
return parts.join(' · ') || t('history.noDescription');
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click', props.item);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
emit('delete', props.item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.playlist-item {
|
||||
@apply flex items-center px-2 py-2 rounded-xl cursor-pointer;
|
||||
@apply transition-all duration-200;
|
||||
@apply bg-light-100 dark:bg-dark-100;
|
||||
@apply hover:bg-light-200 dark:hover:bg-dark-200;
|
||||
@apply mb-2;
|
||||
|
||||
&-img {
|
||||
@apply flex items-center justify-center rounded-xl;
|
||||
@apply w-[60px] h-[60px] flex-shrink-0;
|
||||
@apply bg-light-300 dark:bg-dark-300;
|
||||
}
|
||||
|
||||
&-info {
|
||||
@apply ml-3 flex-1 min-w-0;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply text-gray-900 dark:text-white text-base mb-1;
|
||||
}
|
||||
|
||||
&-desc {
|
||||
@apply text-sm text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-count {
|
||||
@apply px-4 text-lg text-center min-w-[60px];
|
||||
@apply text-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-delete {
|
||||
@apply cursor-pointer rounded-full border-2 w-8 h-8 flex justify-center items-center;
|
||||
@apply border-gray-400 dark:border-gray-600;
|
||||
@apply text-gray-600 dark:text-gray-400;
|
||||
@apply hover:border-red-500 hover:text-red-500;
|
||||
@apply transition-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
63
src/renderer/hooks/AlbumHistoryHook.ts
Normal file
63
src/renderer/hooks/AlbumHistoryHook.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
// 专辑历史记录类型
|
||||
export interface AlbumHistoryItem {
|
||||
id: number;
|
||||
name: string;
|
||||
picUrl?: string;
|
||||
size?: number; // 歌曲数量
|
||||
artist?: {
|
||||
name: string;
|
||||
id: number;
|
||||
};
|
||||
count?: number; // 播放次数
|
||||
lastPlayTime?: number; // 最后播放时间
|
||||
}
|
||||
|
||||
export const useAlbumHistory = () => {
|
||||
const albumHistory = useLocalStorage<AlbumHistoryItem[]>('albumHistory', []);
|
||||
|
||||
const addAlbum = (album: AlbumHistoryItem) => {
|
||||
const index = albumHistory.value.findIndex((item) => item.id === album.id);
|
||||
const now = Date.now();
|
||||
|
||||
if (index !== -1) {
|
||||
// 如果已存在,更新播放次数和时间,并移到最前面
|
||||
albumHistory.value[index].count = (albumHistory.value[index].count || 0) + 1;
|
||||
albumHistory.value[index].lastPlayTime = now;
|
||||
albumHistory.value.unshift(albumHistory.value.splice(index, 1)[0]);
|
||||
} else {
|
||||
// 如果不存在,添加新记录
|
||||
albumHistory.value.unshift({
|
||||
...album,
|
||||
count: 1,
|
||||
lastPlayTime: now
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const delAlbum = (album: AlbumHistoryItem) => {
|
||||
const index = albumHistory.value.findIndex((item) => item.id === album.id);
|
||||
if (index !== -1) {
|
||||
albumHistory.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const albumList = ref(albumHistory.value);
|
||||
|
||||
watch(
|
||||
() => albumHistory.value,
|
||||
() => {
|
||||
albumList.value = albumHistory.value;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
return {
|
||||
albumHistory,
|
||||
albumList,
|
||||
addAlbum,
|
||||
delAlbum
|
||||
};
|
||||
};
|
||||
65
src/renderer/hooks/PlaylistHistoryHook.ts
Normal file
65
src/renderer/hooks/PlaylistHistoryHook.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
// 歌单历史记录类型
|
||||
export interface PlaylistHistoryItem {
|
||||
id: number;
|
||||
name: string;
|
||||
coverImgUrl?: string;
|
||||
picUrl?: string; // 兼容字段
|
||||
trackCount?: number;
|
||||
playCount?: number;
|
||||
creator?: {
|
||||
nickname: string;
|
||||
userId: number;
|
||||
};
|
||||
count?: number; // 播放次数
|
||||
lastPlayTime?: number; // 最后播放时间
|
||||
}
|
||||
|
||||
export const usePlaylistHistory = () => {
|
||||
const playlistHistory = useLocalStorage<PlaylistHistoryItem[]>('playlistHistory', []);
|
||||
|
||||
const addPlaylist = (playlist: PlaylistHistoryItem) => {
|
||||
const index = playlistHistory.value.findIndex((item) => item.id === playlist.id);
|
||||
const now = Date.now();
|
||||
|
||||
if (index !== -1) {
|
||||
// 如果已存在,更新播放次数和时间,并移到最前面
|
||||
playlistHistory.value[index].count = (playlistHistory.value[index].count || 0) + 1;
|
||||
playlistHistory.value[index].lastPlayTime = now;
|
||||
playlistHistory.value.unshift(playlistHistory.value.splice(index, 1)[0]);
|
||||
} else {
|
||||
// 如果不存在,添加新记录
|
||||
playlistHistory.value.unshift({
|
||||
...playlist,
|
||||
count: 1,
|
||||
lastPlayTime: now
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const delPlaylist = (playlist: PlaylistHistoryItem) => {
|
||||
const index = playlistHistory.value.findIndex((item) => item.id === playlist.id);
|
||||
if (index !== -1) {
|
||||
playlistHistory.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const playlistList = ref(playlistHistory.value);
|
||||
|
||||
watch(
|
||||
() => playlistHistory.value,
|
||||
() => {
|
||||
playlistList.value = playlistHistory.value;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
return {
|
||||
playlistHistory,
|
||||
playlistList,
|
||||
addPlaylist,
|
||||
delPlaylist
|
||||
};
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue';
|
||||
|
||||
import { logout } from '@/api/login';
|
||||
import { getLikedList } from '@/api/music';
|
||||
import { getUserAlbumSublist } from '@/api/user';
|
||||
import { getUserAlbumSublist, getUserPlaylist } from '@/api/user';
|
||||
import { clearLoginStatus } from '@/utils/auth';
|
||||
|
||||
interface UserData {
|
||||
@@ -30,6 +30,10 @@ export const useUserStore = defineStore('user', () => {
|
||||
const searchType = ref(1);
|
||||
// 收藏的专辑 ID 列表
|
||||
const collectedAlbumIds = ref<Set<number>>(new Set());
|
||||
// 用户的歌单列表
|
||||
const playList = ref<any[]>([]);
|
||||
// 用户的专辑列表
|
||||
const albumList = ref<any[]>([]);
|
||||
|
||||
// 方法
|
||||
const setUser = (userData: UserData) => {
|
||||
@@ -51,6 +55,9 @@ export const useUserStore = defineStore('user', () => {
|
||||
await logout();
|
||||
user.value = null;
|
||||
loginType.value = null;
|
||||
collectedAlbumIds.value.clear();
|
||||
playList.value = [];
|
||||
albumList.value = [];
|
||||
clearLoginStatus();
|
||||
// 刷新
|
||||
window.location.reload();
|
||||
@@ -59,6 +66,9 @@ export const useUserStore = defineStore('user', () => {
|
||||
// 即使API调用失败,也要清除本地状态
|
||||
user.value = null;
|
||||
loginType.value = null;
|
||||
collectedAlbumIds.value.clear();
|
||||
playList.value = [];
|
||||
albumList.value = [];
|
||||
clearLoginStatus();
|
||||
window.location.reload();
|
||||
}
|
||||
@@ -72,7 +82,41 @@ export const useUserStore = defineStore('user', () => {
|
||||
searchType.value = type;
|
||||
};
|
||||
|
||||
// 初始化收藏的专辑列表
|
||||
// 初始化歌单列表
|
||||
const initializePlaylist = async () => {
|
||||
if (!user.value) {
|
||||
playList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await getUserPlaylist(user.value.userId, 1000, 0);
|
||||
playList.value = data?.playlist || [];
|
||||
console.log(`已加载 ${playList.value.length} 个歌单`);
|
||||
} catch (error) {
|
||||
console.error('获取歌单列表失败:', error);
|
||||
playList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化专辑列表
|
||||
const initializeAlbumList = async () => {
|
||||
if (!user.value || !localStorage.getItem('token')) {
|
||||
albumList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await getUserAlbumSublist({ limit: 1000, offset: 0 });
|
||||
albumList.value = data?.data || [];
|
||||
console.log(`已加载 ${albumList.value.length} 个收藏专辑`);
|
||||
} catch (error) {
|
||||
console.error('获取专辑列表失败:', error);
|
||||
albumList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化收藏的专辑ID列表
|
||||
const initializeCollectedAlbums = async () => {
|
||||
if (!user.value || !localStorage.getItem('token')) {
|
||||
collectedAlbumIds.value.clear();
|
||||
@@ -83,7 +127,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
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} 个收藏专辑`);
|
||||
console.log(`已加载 ${albumIds.length} 个收藏专辑ID`);
|
||||
} catch (error) {
|
||||
console.error('获取收藏专辑列表失败:', error);
|
||||
collectedAlbumIds.value.clear();
|
||||
@@ -113,8 +157,12 @@ export const useUserStore = defineStore('user', () => {
|
||||
// 如果用户已登录,获取收藏列表
|
||||
if (localStorage.getItem('token')) {
|
||||
try {
|
||||
// 同时初始化收藏专辑列表
|
||||
await initializeCollectedAlbums();
|
||||
// 并行加载歌单、专辑和收藏ID列表
|
||||
await Promise.all([
|
||||
initializePlaylist(),
|
||||
initializeAlbumList(),
|
||||
initializeCollectedAlbums()
|
||||
]);
|
||||
|
||||
const { data } = await getLikedList(savedUser.userId);
|
||||
return data?.ids || [];
|
||||
@@ -134,6 +182,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
searchValue,
|
||||
searchType,
|
||||
collectedAlbumIds,
|
||||
playList,
|
||||
albumList,
|
||||
|
||||
// 方法
|
||||
setUser,
|
||||
@@ -142,6 +192,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
setSearchValue,
|
||||
setSearchType,
|
||||
initializeUser,
|
||||
initializePlaylist,
|
||||
initializeAlbumList,
|
||||
initializeCollectedAlbums,
|
||||
addCollectedAlbum,
|
||||
removeCollectedAlbum,
|
||||
|
||||
@@ -3,61 +3,361 @@
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInRight')">
|
||||
{{ t('history.title') }}
|
||||
</div>
|
||||
<!-- 第一级Tab: 歌曲/歌单/专辑 -->
|
||||
<div class="category-tabs-wrapper" :class="setAnimationClass('animate__fadeInRight')">
|
||||
<n-tabs
|
||||
v-model:value="currentCategory"
|
||||
type="segment"
|
||||
animated
|
||||
size="large"
|
||||
@update:value="handleCategoryChange"
|
||||
>
|
||||
<n-tab name="songs" :tab="t('history.categoryTabs.songs')"></n-tab>
|
||||
<n-tab name="playlists" :tab="t('history.categoryTabs.playlists')"></n-tab>
|
||||
<n-tab name="albums" :tab="t('history.categoryTabs.albums')"></n-tab>
|
||||
</n-tabs>
|
||||
</div>
|
||||
<!-- 第二级Tab: 本地/云端 -->
|
||||
<div class="tabs-wrapper" :class="setAnimationClass('animate__fadeInRight')">
|
||||
<n-tabs
|
||||
v-model:value="currentTab"
|
||||
type="segment"
|
||||
animated
|
||||
@update:value="handleTabChange"
|
||||
size="small"
|
||||
>
|
||||
<n-tab name="local" :tab="t('history.tabs.local')"></n-tab>
|
||||
<n-tab name="cloud" :tab="t('history.tabs.cloud')"></n-tab>
|
||||
</n-tabs>
|
||||
</div>
|
||||
<n-scrollbar ref="scrollbarRef" :size="100" @scroll="handleScroll">
|
||||
<div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
||||
<div
|
||||
v-for="(item, index) in displayList"
|
||||
:key="item.id"
|
||||
class="history-item"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 30)"
|
||||
>
|
||||
<song-item class="history-item-content" :item="item" @play="handlePlay" />
|
||||
<div class="history-item-count min-w-[60px]">
|
||||
{{ t('history.playCount', { count: item.count }) }}
|
||||
</div>
|
||||
<div class="history-item-delete">
|
||||
<i class="iconfont icon-close" @click="handleDelMusic(item)"></i>
|
||||
<!-- 歌曲列表 -->
|
||||
<template v-if="currentCategory === 'songs'">
|
||||
<div
|
||||
v-for="(item, index) in displayList"
|
||||
:key="item.id"
|
||||
class="history-item"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 30)"
|
||||
>
|
||||
<song-item class="history-item-content" :item="item" @play="handlePlay" />
|
||||
<div class="history-item-count min-w-[60px]" v-show="currentTab === 'local'">
|
||||
{{ t('history.playCount', { count: item.count }) }}
|
||||
</div>
|
||||
<div class="history-item-delete" v-show="currentTab === 'local'">
|
||||
<i class="iconfont icon-close" @click="handleDelMusic(item)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 歌单列表 -->
|
||||
<template v-if="currentCategory === 'playlists'">
|
||||
<playlist-item
|
||||
v-for="(item, index) in displayList"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:show-count="currentTab === 'local'"
|
||||
:show-delete="currentTab === 'local'"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 30)"
|
||||
@click="handlePlaylistClick(item)"
|
||||
@delete="handleDelPlaylist(item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 专辑列表 -->
|
||||
<template v-if="currentCategory === 'albums'">
|
||||
<album-item
|
||||
v-for="(item, index) in displayList"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:show-count="currentTab === 'local'"
|
||||
:show-delete="currentTab === 'local'"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 30)"
|
||||
@click="handleAlbumClick(item)"
|
||||
@delete="handleDelAlbum(item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div v-if="displayList.length === 0 && !loading" class="no-data">
|
||||
{{ t('history.noData') }}
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<n-spin size="large" />
|
||||
</div>
|
||||
|
||||
<div v-if="noMore" class="no-more-tip">{{ t('common.noMore') }}</div>
|
||||
<div v-if="noMore && displayList.length > 0" class="no-more-tip">
|
||||
{{ t('common.noMore') }}
|
||||
</div>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { processBilibiliVideos } from '@/api/bilibili';
|
||||
import { getListDetail } from '@/api/list';
|
||||
import { getAlbumDetail } from '@/api/music';
|
||||
import { getMusicDetail } from '@/api/music';
|
||||
import { getRecentAlbums, getRecentPlaylists, getRecentSongs } from '@/api/user';
|
||||
import AlbumItem from '@/components/common/AlbumItem.vue';
|
||||
import { navigateToMusicList } from '@/components/common/MusicListNavigator';
|
||||
import PlaylistItem from '@/components/common/PlaylistItem.vue';
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import { useAlbumHistory } from '@/hooks/AlbumHistoryHook';
|
||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||
import { usePlaylistHistory } from '@/hooks/PlaylistHistoryHook';
|
||||
import { usePlayerStore } from '@/store/modules/player';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import type { SongResult } from '@/types/music';
|
||||
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
// 扩展历史记录类型以包含 playTime
|
||||
interface HistoryRecord extends Partial<SongResult> {
|
||||
id: string | number;
|
||||
playTime?: number;
|
||||
score?: number;
|
||||
source?: 'netease' | 'bilibili';
|
||||
count?: number;
|
||||
recordSource?: 'local' | 'cloud';
|
||||
sources?: ('local' | 'cloud')[];
|
||||
bilibiliData?: {
|
||||
bvid: string;
|
||||
cid: number;
|
||||
};
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
const { delMusic, musicList } = useMusicHistory();
|
||||
const { delPlaylist, playlistList } = usePlaylistHistory();
|
||||
const { delAlbum, albumList } = useAlbumHistory();
|
||||
const userStore = useUserStore();
|
||||
const scrollbarRef = ref();
|
||||
const loading = ref(false);
|
||||
const noMore = ref(false);
|
||||
const displayList = ref<SongResult[]>([]);
|
||||
const displayList = ref<any[]>([]);
|
||||
const playerStore = usePlayerStore();
|
||||
const hasLoaded = ref(false);
|
||||
const currentCategory = ref<'songs' | 'playlists' | 'albums'>('songs');
|
||||
const currentTab = ref<'local' | 'cloud'>('local');
|
||||
const cloudRecords = ref<HistoryRecord[]>([]);
|
||||
const cloudPlaylists = ref<any[]>([]);
|
||||
const cloudAlbums = ref<any[]>([]);
|
||||
|
||||
// 无限滚动相关配置
|
||||
const pageSize = 100;
|
||||
const currentPage = ref(1);
|
||||
|
||||
// 获取当前页的音乐详情
|
||||
const getHistorySongs = async () => {
|
||||
if (musicList.value.length === 0) {
|
||||
// 获取云端播放记录
|
||||
const getCloudRecords = async () => {
|
||||
if (!userStore.user?.userId || userStore.loginType !== 'cookie') {
|
||||
message.warning(t('history.needLogin'));
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getRecentSongs(1000);
|
||||
if (res.data?.data?.list) {
|
||||
return res.data.data.list.map((item: any) => ({
|
||||
id: item.data?.id,
|
||||
playTime: item.playTime,
|
||||
source: 'netease',
|
||||
count: 1,
|
||||
data: item.data
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (error: any) {
|
||||
console.error(t('history.getCloudRecordFailed'), error);
|
||||
if (error?.response?.status !== 301 && error?.response?.data?.code !== -2) {
|
||||
message.error(t('history.getCloudRecordFailed'));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取云端歌单播放记录
|
||||
const getCloudPlaylists = async () => {
|
||||
if (!userStore.user?.userId || userStore.loginType !== 'cookie') {
|
||||
message.warning(t('history.needLogin'));
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getRecentPlaylists(100);
|
||||
if (res.data?.data?.list) {
|
||||
return res.data.data.list.map((item: any) => ({
|
||||
id: item.data?.id,
|
||||
name: item.data?.name,
|
||||
coverImgUrl: item.data?.coverImgUrl,
|
||||
picUrl: item.data?.picUrl,
|
||||
trackCount: item.data?.trackCount,
|
||||
playCount: item.data?.playCount,
|
||||
creator: item.data?.creator,
|
||||
playTime: item.playTime
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (error: any) {
|
||||
console.error(t('history.getCloudRecordFailed'), error);
|
||||
if (error?.response?.status !== 301 && error?.response?.data?.code !== -2) {
|
||||
message.error(t('history.getCloudRecordFailed'));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取云端专辑播放记录
|
||||
const getCloudAlbums = async () => {
|
||||
if (!userStore.user?.userId || userStore.loginType !== 'cookie') {
|
||||
message.warning(t('history.needLogin'));
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getRecentAlbums(100);
|
||||
if (res.data?.data?.list) {
|
||||
return res.data.data.list.map((item: any) => ({
|
||||
id: item.data?.id,
|
||||
name: item.data?.name,
|
||||
picUrl: item.data?.picUrl,
|
||||
size: item.data?.size,
|
||||
artist: item.data?.artist,
|
||||
playTime: item.playTime
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
} catch (error: any) {
|
||||
console.error(t('history.getCloudRecordFailed'), error);
|
||||
if (error?.response?.status !== 301 && error?.response?.data?.code !== -2) {
|
||||
message.error(t('history.getCloudRecordFailed'));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 根据当前分类和tab获取要显示的列表
|
||||
const getCurrentList = (): any[] => {
|
||||
if (currentCategory.value === 'songs') {
|
||||
switch (currentTab.value) {
|
||||
case 'local':
|
||||
return musicList.value;
|
||||
case 'cloud':
|
||||
return cloudRecords.value.filter((item) => item.id);
|
||||
}
|
||||
} else if (currentCategory.value === 'playlists') {
|
||||
switch (currentTab.value) {
|
||||
case 'local':
|
||||
return playlistList.value;
|
||||
case 'cloud':
|
||||
return cloudPlaylists.value;
|
||||
}
|
||||
} else if (currentCategory.value === 'albums') {
|
||||
switch (currentTab.value) {
|
||||
case 'local':
|
||||
return albumList.value;
|
||||
case 'cloud':
|
||||
return cloudAlbums.value;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 处理分类切换
|
||||
const handleCategoryChange = async (value: 'songs' | 'playlists' | 'albums') => {
|
||||
currentCategory.value = value;
|
||||
currentPage.value = 1;
|
||||
noMore.value = false;
|
||||
displayList.value = [];
|
||||
|
||||
// 如果切换到云端,且还没有加载对应的云端数据,则加载
|
||||
if (currentTab.value === 'cloud') {
|
||||
loading.value = true;
|
||||
if (value === 'songs' && cloudRecords.value.length === 0) {
|
||||
cloudRecords.value = await getCloudRecords();
|
||||
} else if (value === 'playlists' && cloudPlaylists.value.length === 0) {
|
||||
cloudPlaylists.value = await getCloudPlaylists();
|
||||
} else if (value === 'albums' && cloudAlbums.value.length === 0) {
|
||||
cloudAlbums.value = await getCloudAlbums();
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
await loadHistoryData();
|
||||
};
|
||||
|
||||
// 处理歌单点击
|
||||
const handlePlaylistClick = async (item: any) => {
|
||||
try {
|
||||
const res = await getListDetail(item.id);
|
||||
if (res.data?.playlist) {
|
||||
navigateToMusicList(router, {
|
||||
id: item.id,
|
||||
type: 'playlist',
|
||||
name: item.name,
|
||||
songList: res.data.playlist.tracks || [],
|
||||
listInfo: res.data.playlist,
|
||||
canRemove: false
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('打开歌单失败:', error);
|
||||
message.error('打开歌单失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理专辑点击
|
||||
const handleAlbumClick = async (item: any) => {
|
||||
try {
|
||||
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((song: any) => ({
|
||||
...song,
|
||||
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('打开专辑失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除歌单记录
|
||||
const handleDelPlaylist = (item: any) => {
|
||||
delPlaylist(item);
|
||||
displayList.value = displayList.value.filter((playlist) => playlist.id !== item.id);
|
||||
};
|
||||
|
||||
// 删除专辑记录
|
||||
const handleDelAlbum = (item: any) => {
|
||||
delAlbum(item);
|
||||
displayList.value = displayList.value.filter((album) => album.id !== item.id);
|
||||
};
|
||||
|
||||
// 加载历史数据(根据当前分类)
|
||||
const loadHistoryData = async () => {
|
||||
const currentList = getCurrentList();
|
||||
if (currentList.length === 0) {
|
||||
displayList.value = [];
|
||||
return;
|
||||
}
|
||||
@@ -66,70 +366,77 @@ const getHistorySongs = async () => {
|
||||
try {
|
||||
const startIndex = (currentPage.value - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const currentPageItems = musicList.value.slice(startIndex, endIndex);
|
||||
const currentPageItems = currentList.slice(startIndex, endIndex);
|
||||
|
||||
// 分离网易云音乐和B站视频
|
||||
const neteaseItems = currentPageItems.filter((item) => item.source !== 'bilibili');
|
||||
const bilibiliItems = currentPageItems.filter((item) => item.source === 'bilibili');
|
||||
// 根据分类处理不同的数据
|
||||
if (currentCategory.value === 'songs') {
|
||||
// 处理歌曲数据
|
||||
const neteaseItems = currentPageItems.filter((item) => item.source !== 'bilibili');
|
||||
const bilibiliItems = currentPageItems.filter((item) => item.source === 'bilibili');
|
||||
|
||||
// 处理网易云音乐
|
||||
let neteaseSongs: SongResult[] = [];
|
||||
if (neteaseItems.length > 0) {
|
||||
const currentIds = neteaseItems.map((item) => item.id as number);
|
||||
const res = await getMusicDetail(currentIds);
|
||||
if (res.data.songs) {
|
||||
neteaseSongs = res.data.songs.map((song: SongResult) => {
|
||||
const historyItem = neteaseItems.find((item) => item.id === song.id);
|
||||
return {
|
||||
...song,
|
||||
picUrl: song.al?.picUrl || '',
|
||||
count: historyItem?.count || 0,
|
||||
source: 'netease'
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理B站视频 - 使用公用方法
|
||||
const bilibiliIds = bilibiliItems
|
||||
.map((item) => `${item.bilibiliData?.bvid}--1--${item.bilibiliData?.cid}`)
|
||||
.filter((id) => id && !id.includes('undefined'));
|
||||
|
||||
const bilibiliSongs = await processBilibiliVideos(bilibiliIds);
|
||||
|
||||
// 添加count信息
|
||||
bilibiliSongs.forEach((song) => {
|
||||
const historyItem = bilibiliItems.find(
|
||||
(item) =>
|
||||
item.bilibiliData?.bvid === song.bilibiliData?.bvid &&
|
||||
item.bilibiliData?.cid === song.bilibiliData?.cid
|
||||
);
|
||||
if (historyItem) {
|
||||
song.count = historyItem.count || 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 合并两种来源的数据,并保持原有顺序
|
||||
const newSongs = currentPageItems
|
||||
.map((item) => {
|
||||
if (item.source === 'bilibili') {
|
||||
return bilibiliSongs.find(
|
||||
(song) =>
|
||||
song.bilibiliData?.bvid === item.bilibiliData?.bvid &&
|
||||
song.bilibiliData?.cid === item.bilibiliData?.cid
|
||||
);
|
||||
let neteaseSongs: SongResult[] = [];
|
||||
if (neteaseItems.length > 0) {
|
||||
const currentIds = neteaseItems.map((item) => item.id as number);
|
||||
const res = await getMusicDetail(currentIds);
|
||||
if (res.data.songs) {
|
||||
neteaseSongs = res.data.songs.map((song: SongResult) => {
|
||||
const historyItem = neteaseItems.find((item) => item.id === song.id);
|
||||
return {
|
||||
...song,
|
||||
picUrl: song.al?.picUrl || '',
|
||||
count: historyItem?.count || 0,
|
||||
source: 'netease'
|
||||
};
|
||||
});
|
||||
}
|
||||
return neteaseSongs.find((song) => song.id === item.id);
|
||||
})
|
||||
.filter((song): song is SongResult => !!song);
|
||||
}
|
||||
|
||||
if (currentPage.value === 1) {
|
||||
displayList.value = newSongs;
|
||||
const bilibiliIds = bilibiliItems
|
||||
.map((item) => `${item.bilibiliData?.bvid}--1--${item.bilibiliData?.cid}`)
|
||||
.filter((id) => id && !id.includes('undefined'));
|
||||
|
||||
const bilibiliSongs = await processBilibiliVideos(bilibiliIds);
|
||||
|
||||
bilibiliSongs.forEach((song) => {
|
||||
const historyItem = bilibiliItems.find(
|
||||
(item) =>
|
||||
item.bilibiliData?.bvid === song.bilibiliData?.bvid &&
|
||||
item.bilibiliData?.cid === song.bilibiliData?.cid
|
||||
);
|
||||
if (historyItem) {
|
||||
song.count = historyItem.count || 0;
|
||||
}
|
||||
});
|
||||
|
||||
const newSongs = currentPageItems
|
||||
.map((item) => {
|
||||
if (item.source === 'bilibili') {
|
||||
return bilibiliSongs.find(
|
||||
(song) =>
|
||||
song.bilibiliData?.bvid === item.bilibiliData?.bvid &&
|
||||
song.bilibiliData?.cid === item.bilibiliData?.cid
|
||||
);
|
||||
}
|
||||
return neteaseSongs.find((song) => song.id === item.id);
|
||||
})
|
||||
.filter((song): song is SongResult => !!song);
|
||||
|
||||
if (currentPage.value === 1) {
|
||||
displayList.value = newSongs;
|
||||
} else {
|
||||
displayList.value = [...displayList.value, ...newSongs];
|
||||
}
|
||||
} else {
|
||||
displayList.value = [...displayList.value, ...newSongs];
|
||||
// 处理歌单和专辑数据(直接显示,不需要额外请求)
|
||||
if (currentPage.value === 1) {
|
||||
displayList.value = currentPageItems;
|
||||
} else {
|
||||
displayList.value = [...displayList.value, ...currentPageItems];
|
||||
}
|
||||
}
|
||||
|
||||
noMore.value = displayList.value.length >= musicList.value.length;
|
||||
const totalLength = getCurrentList().length;
|
||||
noMore.value = displayList.value.length >= totalLength;
|
||||
} catch (error) {
|
||||
console.error(t('history.getHistoryFailed'), error);
|
||||
} finally {
|
||||
@@ -140,11 +447,11 @@ const getHistorySongs = async () => {
|
||||
// 处理滚动事件
|
||||
const handleScroll = (e: any) => {
|
||||
const { scrollTop, scrollHeight, offsetHeight } = e.target;
|
||||
const threshold = 100; // 距离底部多少像素时加载更多
|
||||
const threshold = 100;
|
||||
|
||||
if (!loading.value && !noMore.value && scrollHeight - (scrollTop + offsetHeight) < threshold) {
|
||||
currentPage.value++;
|
||||
getHistorySongs();
|
||||
loadHistoryData();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,22 +460,45 @@ const handlePlay = () => {
|
||||
playerStore.setPlayList(displayList.value);
|
||||
};
|
||||
|
||||
// 处理 tab 切换
|
||||
const handleTabChange = async (value: 'local' | 'cloud') => {
|
||||
currentTab.value = value;
|
||||
currentPage.value = 1;
|
||||
noMore.value = false;
|
||||
displayList.value = [];
|
||||
|
||||
// 如果切换到云端,且还没有加载对应的云端数据,则加载
|
||||
if (value === 'cloud') {
|
||||
loading.value = true;
|
||||
if (currentCategory.value === 'songs' && cloudRecords.value.length === 0) {
|
||||
cloudRecords.value = await getCloudRecords();
|
||||
} else if (currentCategory.value === 'playlists' && cloudPlaylists.value.length === 0) {
|
||||
cloudPlaylists.value = await getCloudPlaylists();
|
||||
} else if (currentCategory.value === 'albums' && cloudAlbums.value.length === 0) {
|
||||
cloudAlbums.value = await getCloudAlbums();
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
await loadHistoryData();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (!hasLoaded.value) {
|
||||
await getHistorySongs();
|
||||
await loadHistoryData();
|
||||
hasLoaded.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听历史列表变化,变化时重置并重新加载
|
||||
watch(
|
||||
musicList,
|
||||
[musicList, playlistList, albumList],
|
||||
async () => {
|
||||
hasLoaded.value = false;
|
||||
currentPage.value = 1;
|
||||
noMore.value = false;
|
||||
await getHistorySongs();
|
||||
hasLoaded.value = true;
|
||||
if (hasLoaded.value) {
|
||||
currentPage.value = 1;
|
||||
noMore.value = false;
|
||||
await loadHistoryData();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
@@ -191,6 +521,14 @@ const handleDelMusic = async (item: SongResult) => {
|
||||
@apply text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
.category-tabs-wrapper {
|
||||
@apply px-4 mb-2;
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
@apply px-4;
|
||||
}
|
||||
|
||||
.history-list-content {
|
||||
@apply mt-2 pb-28 px-4;
|
||||
.history-item {
|
||||
@@ -220,4 +558,27 @@ const handleDelMusic = async (item: SongResult) => {
|
||||
@apply text-center py-4 text-sm;
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
@apply text-center py-8 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
:deep(.n-tabs-rail) {
|
||||
@apply rounded-xl overflow-hidden !important;
|
||||
.n-tabs-capsule {
|
||||
@apply rounded-xl !important;
|
||||
}
|
||||
}
|
||||
|
||||
.category-tabs-wrapper {
|
||||
:deep(.n-tabs-rail) {
|
||||
@apply rounded-xl overflow-hidden bg-white dark:bg-dark-300 !important;
|
||||
.n-tabs-capsule {
|
||||
@apply rounded-xl bg-green-500 dark:bg-green-600 !important;
|
||||
}
|
||||
.n-tabs-tab--active {
|
||||
@apply text-white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -239,6 +239,8 @@ import {
|
||||
} from '@/api/music';
|
||||
import PlayBottom from '@/components/common/PlayBottom.vue';
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import { useAlbumHistory } from '@/hooks/AlbumHistoryHook';
|
||||
import { usePlaylistHistory } from '@/hooks/PlaylistHistoryHook';
|
||||
import { useDownload } from '@/hooks/useDownload';
|
||||
import { useMusicStore, usePlayerStore, useRecommendStore, useUserStore } from '@/store';
|
||||
import { SongResult } from '@/types/music';
|
||||
@@ -252,6 +254,8 @@ const musicStore = useMusicStore();
|
||||
const recommendStore = useRecommendStore();
|
||||
const userStore = useUserStore();
|
||||
const message = useMessage();
|
||||
const { addPlaylist } = usePlaylistHistory();
|
||||
const { addAlbum } = useAlbumHistory();
|
||||
|
||||
// 从路由参数或状态管理获取数据
|
||||
const loading = ref(false);
|
||||
@@ -631,7 +635,7 @@ const handlePlay = async () => {
|
||||
playerStore.setPlayList(filteredSongs.value.map(formatSong));
|
||||
return;
|
||||
}
|
||||
|
||||
saveHistory();
|
||||
// 如果完整播放列表已加载完成
|
||||
if (isFullPlaylistLoaded.value && completePlaylist.value.length > 0) {
|
||||
playerStore.setPlayList(completePlaylist.value.map(formatSong));
|
||||
@@ -874,11 +878,27 @@ const toggleCollect = async () => {
|
||||
|
||||
// 更新收藏状态
|
||||
if (type === 'album') {
|
||||
// 专辑:更新 store 中的收藏状态
|
||||
// 专辑:更新 store 中的收藏状态和专辑列表
|
||||
if (isCollected.value) {
|
||||
// 添加到收藏ID集合
|
||||
userStore.addCollectedAlbum(listInfo.value.id);
|
||||
// 添加到专辑列表
|
||||
const albumData = {
|
||||
id: listInfo.value.id,
|
||||
name: listInfo.value.name,
|
||||
picUrl: listInfo.value.picUrl || listInfo.value.coverImgUrl,
|
||||
size: listInfo.value.size,
|
||||
artist: listInfo.value.artist || listInfo.value.artists?.[0]
|
||||
};
|
||||
userStore.albumList.unshift(albumData);
|
||||
} else {
|
||||
// 从收藏ID集合中移除
|
||||
userStore.removeCollectedAlbum(listInfo.value.id);
|
||||
// 从专辑列表中移除
|
||||
const index = userStore.albumList.findIndex((album) => album.id === listInfo.value.id);
|
||||
if (index !== -1) {
|
||||
userStore.albumList.splice(index, 1);
|
||||
}
|
||||
}
|
||||
(listInfo.value as any).isSub = isCollected.value;
|
||||
} else {
|
||||
@@ -899,7 +919,7 @@ const toggleCollect = async () => {
|
||||
// 播放全部
|
||||
const handlePlayAll = () => {
|
||||
if (displayedSongs.value.length === 0) return;
|
||||
|
||||
saveHistory();
|
||||
// 如果有搜索关键词,只播放过滤后的歌曲
|
||||
if (searchKeyword.value) {
|
||||
playerStore.setPlayList(filteredSongs.value.map(formatSong));
|
||||
@@ -914,6 +934,31 @@ const handlePlayAll = () => {
|
||||
playerStore.setPlay(formatSong(displayedSongs.value[0]));
|
||||
};
|
||||
|
||||
const saveHistory = () => {
|
||||
if (listInfo.value?.id) {
|
||||
if (isAlbum.value) {
|
||||
// 保存专辑播放记录
|
||||
addAlbum({
|
||||
id: listInfo.value.id,
|
||||
name: listInfo.value.name || '',
|
||||
picUrl: listInfo.value.picUrl || listInfo.value.coverImgUrl || '',
|
||||
size: listInfo.value.size || displayedSongs.value.length,
|
||||
artist: listInfo.value.artist || listInfo.value.artists?.[0]
|
||||
});
|
||||
} else if (route.query.type === 'playlist') {
|
||||
// 保存歌单播放记录
|
||||
addPlaylist({
|
||||
id: listInfo.value.id,
|
||||
name: listInfo.value.name || '',
|
||||
coverImgUrl: listInfo.value.coverImgUrl || listInfo.value.picUrl || '',
|
||||
trackCount: listInfo.value.trackCount || displayedSongs.value.length,
|
||||
playCount: listInfo.value.playCount,
|
||||
creator: listInfo.value.creator
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 添加到播放列表末尾
|
||||
const addToPlaylist = () => {
|
||||
if (displayedSongs.value.length === 0) return;
|
||||
|
||||
@@ -144,7 +144,6 @@ const userStore = useUserStore();
|
||||
const playerStore = usePlayerStore();
|
||||
const router = useRouter();
|
||||
const userDetail = ref<IUserDetail>();
|
||||
const playList = ref<any[]>([]);
|
||||
const recordList = ref();
|
||||
const infoLoading = ref(false);
|
||||
const mounted = ref(true);
|
||||
@@ -159,34 +158,27 @@ const tabs = [
|
||||
{ key: 'album', label: 'user.tabs.album' }
|
||||
];
|
||||
const currentTab = ref('created');
|
||||
const albumList = ref<any[]>([]);
|
||||
|
||||
const user = computed(() => userStore.user);
|
||||
|
||||
// 创建的歌单(当前用户创建的)
|
||||
const createdPlaylists = computed(() => {
|
||||
if (!user.value) return [];
|
||||
return playList.value.filter((item) => item.creator?.userId === user.value!.userId);
|
||||
return userStore.playList.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);
|
||||
return userStore.playList.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 [];
|
||||
if (currentTab.value === 'album') {
|
||||
return userStore.albumList;
|
||||
}
|
||||
return currentTab.value === 'created' ? createdPlaylists.value : favoritePlaylists.value;
|
||||
});
|
||||
|
||||
// 获取封面图片 URL
|
||||
@@ -226,28 +218,18 @@ onBeforeUnmount(() => {
|
||||
|
||||
// 检查登录状态
|
||||
const checkLoginStatus = () => {
|
||||
// 优先使用 userStore 中的状态
|
||||
// userStore 的状态已经在 App.vue 中全局初始化,这里只需要检查
|
||||
if (userStore.user && userStore.loginType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果 store 中没有数据,尝试从 localStorage 恢复
|
||||
// 如果还是没有登录信息,跳转到登录页
|
||||
const loginInfo = checkAuthStatus();
|
||||
|
||||
if (!loginInfo.isLoggedIn) {
|
||||
!isMobile.value && router.push('/login');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 恢复用户数据和登录类型到 store
|
||||
if (!userStore.user && loginInfo.user) {
|
||||
userStore.setUser(loginInfo.user);
|
||||
}
|
||||
|
||||
if (!userStore.loginType && loginInfo.loginType) {
|
||||
userStore.setLoginType(loginInfo.loginType);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -270,22 +252,28 @@ const loadData = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 Promise.all 并行请求提高效率
|
||||
const [userDetailRes, playlistRes, recordRes] = await Promise.all([
|
||||
getUserDetail(user.value.userId),
|
||||
getUserPlaylist(user.value.userId),
|
||||
getUserRecord(user.value.userId)
|
||||
]);
|
||||
// 如果 store 中还没有数据,则加载
|
||||
const promises = [getUserDetail(user.value.userId), getUserRecord(user.value.userId)];
|
||||
|
||||
if (userStore.playList.length === 0) {
|
||||
promises.push(getUserPlaylist(user.value.userId));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
if (!mounted.value) return;
|
||||
|
||||
userDetail.value = userDetailRes.data;
|
||||
playList.value = playlistRes.data.playlist;
|
||||
recordList.value = recordRes.data.allData.map((item: any) => ({
|
||||
userDetail.value = results[0].data;
|
||||
recordList.value = results[1].data.allData.map((item: any) => ({
|
||||
...item,
|
||||
...item.song,
|
||||
picUrl: item.song.al.picUrl
|
||||
}));
|
||||
|
||||
// 如果加载了歌单,更新 store
|
||||
if (results.length > 2 && results[2].data?.playlist) {
|
||||
userStore.playList = results[2].data.playlist;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载用户页面失败:', error);
|
||||
if (error.response?.status === 401) {
|
||||
@@ -304,11 +292,17 @@ const loadData = async () => {
|
||||
|
||||
// 加载专辑列表
|
||||
const loadAlbumList = async () => {
|
||||
// 如果 store 中已经有数据,直接返回
|
||||
if (userStore.albumList.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
infoLoading.value = true;
|
||||
const res = await getUserAlbumSublist({ limit: 100, offset: 0 });
|
||||
if (!mounted.value) return;
|
||||
albumList.value = res.data.data || [];
|
||||
// 更新 store 中的专辑列表
|
||||
userStore.albumList = res.data.data || [];
|
||||
} catch (error: any) {
|
||||
console.error('加载专辑列表失败:', error);
|
||||
message.error('加载专辑列表失败');
|
||||
@@ -348,8 +342,8 @@ watch(currentTab, async (newTab) => {
|
||||
if (newTab === 'album') {
|
||||
// 刷新收藏专辑列表到 store
|
||||
await userStore.initializeCollectedAlbums();
|
||||
// 如果本地列表为空,则加载
|
||||
if (albumList.value.length === 0) {
|
||||
// 如果 store 中列表为空,则加载
|
||||
if (userStore.albumList.length === 0) {
|
||||
loadAlbumList();
|
||||
}
|
||||
}
|
||||
@@ -487,7 +481,7 @@ const currentLoginType = computed(() => userStore.loginType);
|
||||
.record-list {
|
||||
@apply rounded-2xl;
|
||||
@apply bg-light dark:bg-black;
|
||||
height: calc(100% - 100px);
|
||||
height: calc(100% - 60px);
|
||||
|
||||
.record-item {
|
||||
@apply flex items-center px-2 mb-2 rounded-2xl bg-light-100 dark:bg-dark-100;
|
||||
|
||||
Reference in New Issue
Block a user