refactor: 调整历史/收藏/列表/用户页面

This commit is contained in:
alger
2026-02-04 20:16:36 +08:00
parent 83a6e9381c
commit 423167b9b3
6 changed files with 756 additions and 819 deletions
+6 -20
View File
@@ -132,12 +132,10 @@ import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { getListDetail } from '@/api/list';
import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';
import { navigateToMusicList } from '@/components/common/MusicListNavigator';
import SongItem from '@/components/common/SongItem.vue';
import { usePlayerStore } from '@/store/modules/player';
import type { Playlist } from '@/types/listDetail';
import type { IUserDetail } from '@/types/user';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
@@ -160,10 +158,6 @@ const recordList = ref<any[]>([]);
const loading = ref(true);
const hasRecordPermission = ref(true); // 是否有权限查看听歌记录
// 歌单详情相关
const currentList = ref<Playlist>();
const listLoading = ref(false);
// 加载用户数据
const loadUserData = async () => {
if (!userId.value) {
@@ -238,20 +232,12 @@ watch(
// 替换显示歌单的方法
const openPlaylist = (item: any) => {
listLoading.value = true;
getListDetail(item.id).then((res) => {
currentList.value = res.data.playlist;
listLoading.value = false;
navigateToMusicList(router, {
id: item.id,
type: 'playlist',
name: item.name,
songList: res.data.playlist.tracks || [],
listInfo: res.data.playlist,
canRemove: false
});
navigateToMusicList(router, {
id: item.id,
type: 'playlist',
name: item.name,
listInfo: item,
canRemove: false
});
};
+178 -149
View File
@@ -1,110 +1,166 @@
<template>
<div class="user-page">
<div
v-if="userDetail && user"
class="left"
:class="setAnimationClass('animate__fadeInLeft')"
:style="{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }"
>
<div class="page">
<div class="user-name">
<span>{{ user.nickname }}</span>
<span v-if="currentLoginType" class="login-type">{{
t('login.title.' + currentLoginType)
}}</span>
</div>
<div class="user-info">
<n-avatar round :size="50" :src="getImgUrl(user.avatarUrl, '50y50')" />
<div class="user-info-list">
<div class="user-info-item">
<div class="label">{{ userDetail.profile.followeds }}</div>
<div>{{ t('user.profile.followers') }}</div>
</div>
<div class="user-info-item" @click="showFollowList">
<div class="label">{{ userDetail.profile.follows }}</div>
<div>{{ t('user.profile.following') }}</div>
</div>
<div class="user-info-item">
<div class="label">{{ userDetail.level }}</div>
<div>{{ t('user.profile.level') }}</div>
<template v-if="infoLoading">
<div class="left bg-gray-200 p-4 dark:bg-gray-800">
<div class="flex flex-col gap-6">
<div class="flex justify-between">
<n-skeleton text class="h-8 w-32" />
<n-skeleton text class="h-6 w-20" />
</div>
<div class="flex items-center gap-4">
<n-skeleton class="h-[50px] w-[50px] rounded-full" />
<div class="flex w-2/5 justify-around">
<div v-for="i in 3" :key="i" class="flex flex-col items-center gap-1">
<n-skeleton text class="h-5 w-8" />
<n-skeleton text class="h-4 w-12" />
</div>
</div>
</div>
</div>
<div class="uesr-signature">{{ userDetail.profile.signature }}</div>
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
<div class="tab-container">
<n-tabs v-model:value="currentTab" type="segment" animated>
<n-tab v-for="tab in tabs" :key="tab.key" :name="tab.key" :tab="t(tab.label)">
</n-tab>
</n-tabs>
</div>
<n-scrollbar>
<div class="mt-4">
<button
class="play-list-item"
@click="goToImportPlaylist"
v-if="isElectron && currentTab === 'created'"
>
<div class="play-list-item-img"><i class="icon iconfont ri-add-line"></i></div>
<div class="play-list-item-info">
<div class="play-list-item-name">
{{ t('comp.playlist.import.button') }}
</div>
</div>
</button>
<div
v-for="(item, index) in currentList"
:key="index"
class="play-list-item"
@click="handleItemClick(item)"
>
<n-image
:src="getImgUrl(getCoverUrl(item), '50y50')"
class="play-list-item-img"
lazy
preview-disabled
/>
<div class="play-list-item-info">
<div class="play-list-item-name">
<n-ellipsis :line-clamp="1">{{ item.name }}</n-ellipsis>
</div>
<div class="play-list-item-count">
{{ getItemDescription(item) }}
</div>
<n-skeleton text class="h-4 w-3/4" />
<div class="mt-4 rounded-xl bg-light p-4 dark:bg-black">
<n-skeleton class="mb-4 h-8 w-full rounded-xl" />
<div class="space-y-4">
<div v-for="i in 5" :key="i" class="flex gap-3">
<n-skeleton class="h-[50px] w-[50px] rounded-xl" />
<div class="flex flex-1 flex-col justify-center gap-2">
<n-skeleton text class="h-4 w-1/2" />
<n-skeleton text class="h-3 w-1/3" />
</div>
</div>
<div class="pb-20"></div>
<play-bottom />
</div>
</div>
</div>
</div>
<div v-if="!isMobile" class="right">
<div class="title"><n-skeleton text class="h-8 w-32" /></div>
<div class="rounded-2xl bg-light p-4 dark:bg-black">
<div class="space-y-2">
<div
v-for="i in 10"
:key="i"
class="flex items-center gap-4 rounded-2xl bg-light-100 p-2 dark:bg-dark-100"
>
<n-skeleton class="h-10 w-10 rounded-full" />
<n-skeleton class="h-10 w-10 rounded-xl" />
<div class="flex flex-1 flex-col gap-2">
<n-skeleton text class="h-4 w-1/3" />
<n-skeleton text class="h-3 w-1/4" />
</div>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<div
v-if="userDetail && user"
class="left"
:class="setAnimationClass('animate__fadeIn')"
:style="{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }"
>
<div class="page">
<div class="user-name">
<span>{{ user.nickname }}</span>
<span v-if="currentLoginType" class="login-type">{{
t('login.title.' + currentLoginType)
}}</span>
</div>
<div class="user-info">
<n-avatar round :size="50" :src="getImgUrl(user.avatarUrl, '50y50')" />
<div class="user-info-list">
<div class="user-info-item">
<div class="label">{{ userDetail.profile.followeds }}</div>
<div>{{ t('user.profile.followers') }}</div>
</div>
<div class="user-info-item" @click="showFollowList">
<div class="label">{{ userDetail.profile.follows }}</div>
<div>{{ t('user.profile.following') }}</div>
</div>
<div class="user-info-item">
<div class="label">{{ userDetail.level }}</div>
<div>{{ t('user.profile.level') }}</div>
</div>
</div>
</div>
<div class="uesr-signature">{{ userDetail.profile.signature }}</div>
<div class="play-list" :class="setAnimationClass('animate__fadeIn')">
<div class="tab-container">
<n-tabs v-model:value="currentTab" type="segment" animated>
<n-tab v-for="tab in tabs" :key="tab.key" :name="tab.key" :tab="t(tab.label)">
</n-tab>
</n-tabs>
</div>
<n-scrollbar>
<div class="mt-4">
<div
v-if="albumLoading && currentTab === 'album'"
class="flex h-32 items-center justify-center"
>
<n-spin size="medium" />
</div>
<template v-else>
<button
class="play-list-item"
@click="goToImportPlaylist"
v-if="isElectron && currentTab === 'created'"
>
<div class="play-list-item-img"><i class="icon iconfont ri-add-line"></i></div>
<div class="play-list-item-info">
<div class="play-list-item-name">
{{ t('comp.playlist.import.button') }}
</div>
</div>
</button>
<div
v-for="(item, index) in currentList"
:key="index"
class="play-list-item"
@click="handleItemClick(item)"
>
<n-image
:src="getImgUrl(getCoverUrl(item), '50y50')"
class="play-list-item-img"
lazy
preview-disabled
/>
<div class="play-list-item-info">
<div class="play-list-item-name">
<n-ellipsis :line-clamp="1">{{ item.name }}</n-ellipsis>
</div>
<div class="play-list-item-count">
{{ getItemDescription(item) }}
</div>
</div>
</div>
<div class="pb-20"></div>
<play-bottom />
</template>
</div>
</n-scrollbar>
</div>
</div>
</div>
<div v-if="!isMobile" class="right" :class="setAnimationClass('animate__fadeIn')">
<div class="title">{{ t('user.ranking.title') }}</div>
<div class="record-list">
<n-scrollbar>
<div
v-for="(item, index) in recordList"
:key="item.id"
class="record-item"
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 25)"
>
<div class="play-score">
{{ index + 1 }}
</div>
<song-item class="song-item" :item="item" mini @play="handlePlay" />
</div>
<play-bottom />
</n-scrollbar>
</div>
</div>
</div>
<div
v-if="!isMobile"
v-loading="infoLoading"
class="right"
:class="setAnimationClass('animate__fadeInRight')"
>
<div class="title">{{ t('user.ranking.title') }}</div>
<div class="record-list">
<n-scrollbar>
<div
v-for="(item, index) in recordList"
:key="item.id"
class="record-item"
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 25)"
>
<div class="play-score">
{{ index + 1 }}
</div>
<song-item class="song-item" :item="item" mini @play="handlePlay" />
</div>
<play-bottom />
</n-scrollbar>
</div>
</div>
</template>
<!-- 未登录时显示登录组件 -->
<div
v-if="!isLoggedIn && isMobile"
@@ -118,19 +174,17 @@
<script lang="ts" setup>
import { useMessage } from 'naive-ui';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { getListDetail } from '@/api/list';
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';
import { usePlayerStore } from '@/store/modules/player';
import { useUserStore } from '@/store/modules/user';
import type { Playlist } from '@/types/listDetail';
import type { IUserDetail } from '@/types/user';
import { getImgUrl, isElectron, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
import { checkLoginStatus as checkAuthStatus } from '@/utils/auth';
import LoginComponent from '@/views/login/index.vue';
@@ -143,12 +197,10 @@ const { t } = useI18n();
const userStore = useUserStore();
const playerStore = usePlayerStore();
const router = useRouter();
const userDetail = ref<IUserDetail>();
const recordList = ref();
const { userDetail, recordList } = storeToRefs(userStore);
const infoLoading = ref(false);
const albumLoading = ref(false);
const mounted = ref(true);
const list = ref<Playlist>();
const listLoading = ref(false);
const message = useMessage();
// Tab 相关
@@ -244,7 +296,10 @@ const loadPage = async () => {
const loadData = async () => {
try {
infoLoading.value = true;
// 只有在没有数据时才显示加载状态
if (!userDetail.value || !recordList.value?.length) {
infoLoading.value = true;
}
if (!user.value) {
console.warn('用户数据不存在,尝试重新获取');
@@ -298,7 +353,7 @@ const loadAlbumList = async () => {
}
try {
infoLoading.value = true;
albumLoading.value = true;
const res = await getUserAlbumSublist({ limit: 100, offset: 0 });
if (!mounted.value) return;
// 更新 store 中的专辑列表
@@ -308,7 +363,7 @@ const loadAlbumList = async () => {
message.error('加载专辑列表失败');
} finally {
if (mounted.value) {
infoLoading.value = false;
albumLoading.value = false;
}
}
};
@@ -356,53 +411,27 @@ onMounted(() => {
// 替换显示歌单的方法
const openPlaylist = (item: any) => {
listLoading.value = true;
getListDetail(item.id).then((res) => {
list.value = res.data.playlist;
listLoading.value = false;
navigateToMusicList(router, {
id: item.id,
type: 'playlist',
name: item.name,
songList: res.data.playlist.tracks || [],
listInfo: res.data.playlist,
canRemove: true // 保留可移除功能
});
navigateToMusicList(router, {
id: item.id,
type: 'playlist',
name: item.name,
listInfo: item,
canRemove: true // 保留可移除功能
});
};
// 打开专辑
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;
}
navigateToMusicList(router, {
id: item.id,
type: 'album',
name: item.name,
listInfo: {
...item,
coverImgUrl: item.picUrl || item.coverImgUrl
},
canRemove: false // 专辑不支持移除歌曲
});
};
const handlePlay = () => {