mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-18 19:47:29 +08:00
✨ feat: 优化列表渲染
This commit is contained in:
@@ -10,77 +10,69 @@
|
|||||||
>
|
>
|
||||||
<div class="music-page">
|
<div class="music-page">
|
||||||
<div class="music-close">
|
<div class="music-close">
|
||||||
<i class="icon ri-layout-column-line" @click="doubleDisply = !doubleDisply"></i>
|
|
||||||
<i class="icon iconfont icon-icon_error" @click="close"></i>
|
<i class="icon iconfont icon-icon_error" @click="close"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="music-title text-el">{{ name }}</div>
|
<div class="music-title text-el">{{ name }}</div>
|
||||||
<!-- 歌单歌曲列表 -->
|
<!-- 歌单歌曲列表 -->
|
||||||
<div class="music-list">
|
<div v-loading="loading" class="music-list">
|
||||||
<n-scrollbar @scroll="handleScroll">
|
<n-virtual-list
|
||||||
<div
|
v-if="displayedSongs.length"
|
||||||
v-loading="loading || !songList.length"
|
ref="virtualListRef"
|
||||||
class="music-list-content"
|
:items="displayedSongs"
|
||||||
:class="{ 'double-list': doubleDisply }"
|
:item-size="60"
|
||||||
>
|
:keep-alive="true"
|
||||||
<div
|
:min-size="5"
|
||||||
v-for="(item, index) in displayedSongs"
|
:style="{ height: listHeight }"
|
||||||
:key="item.id"
|
@scroll="handleScroll"
|
||||||
class="double-item"
|
|
||||||
:class="setAnimationClass('animate__bounceInUp')"
|
|
||||||
:style="setAnimationDelay(index, 5)"
|
|
||||||
>
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||||
</div>
|
|
||||||
<div v-if="isLoadingMore" class="loading-more">加载更多...</div>
|
|
||||||
</div>
|
|
||||||
<play-bottom />
|
|
||||||
</n-scrollbar>
|
|
||||||
|
|
||||||
<!-- <n-virtual-list :item-size="42" :items="displayedSongs" item-resizable @scroll="handleScroll">
|
|
||||||
<template #default="{ item, index }">
|
|
||||||
<div :key="item.id" class="double-item">
|
|
||||||
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</n-virtual-list>
|
</n-virtual-list>
|
||||||
<play-bottom /> -->
|
<div v-else-if="loading" class="loading-more">加载中...</div>
|
||||||
|
<play-bottom />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
// 导入 NVirtualListInst 类型
|
||||||
|
import type { VirtualListInst } from 'naive-ui';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import { getMusicDetail } from '@/api/music';
|
import { getMusicDetail } from '@/api/music';
|
||||||
import SongItem from '@/components/common/SongItem.vue';
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
|
import { isMobile } from '@/utils';
|
||||||
|
|
||||||
import PlayBottom from './common/PlayBottom.vue';
|
import PlayBottom from './common/PlayBottom.vue';
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
const {
|
const props = defineProps<{
|
||||||
songList,
|
|
||||||
loading = false,
|
|
||||||
listInfo,
|
|
||||||
} = defineProps<{
|
|
||||||
show: boolean;
|
show: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
songList: any[];
|
songList: any[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
listInfo?: any;
|
listInfo?: {
|
||||||
|
trackIds: { id: number }[];
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits(['update:show', 'update:loading']);
|
const emit = defineEmits(['update:show', 'update:loading']);
|
||||||
|
|
||||||
const page = ref(0);
|
const page = ref(0);
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const total = ref(0);
|
|
||||||
const isLoadingMore = ref(false);
|
const isLoadingMore = ref(false);
|
||||||
const displayedSongs = ref<any[]>([]);
|
const displayedSongs = ref<any[]>([]);
|
||||||
|
|
||||||
// 双排显示开关
|
// 计算总数
|
||||||
const doubleDisply = ref(false);
|
const total = computed(() => {
|
||||||
|
if (props.listInfo?.trackIds) {
|
||||||
|
return props.listInfo.trackIds.length;
|
||||||
|
}
|
||||||
|
return props.songList.length;
|
||||||
|
});
|
||||||
|
|
||||||
const formatDetail = computed(() => (detail: any) => {
|
const formatDetail = computed(() => (detail: any) => {
|
||||||
const song = {
|
const song = {
|
||||||
@@ -95,7 +87,7 @@ const formatDetail = computed(() => (detail: any) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
const tracks = songList || [];
|
const tracks = props.songList || [];
|
||||||
store.commit(
|
store.commit(
|
||||||
'setPlayList',
|
'setPlayList',
|
||||||
tracks.map((item) => ({
|
tracks.map((item) => ({
|
||||||
@@ -112,40 +104,70 @@ const close = () => {
|
|||||||
emit('update:show', false);
|
emit('update:show', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 优化加载更多歌曲的函数
|
||||||
const loadMoreSongs = async () => {
|
const loadMoreSongs = async () => {
|
||||||
if (displayedSongs.value.length >= total.value) return;
|
if (isLoadingMore.value || displayedSongs.value.length >= total.value) return;
|
||||||
|
|
||||||
isLoadingMore.value = true;
|
isLoadingMore.value = true;
|
||||||
try {
|
try {
|
||||||
const trackIds = listInfo.trackIds
|
if (props.listInfo?.trackIds) {
|
||||||
.slice(page.value * pageSize, (page.value + 1) * pageSize)
|
// 如果有 trackIds,需要分批请求歌曲详情
|
||||||
.map((item: any) => item.id);
|
const start = page.value * pageSize;
|
||||||
const reslist = await getMusicDetail(trackIds);
|
const end = Math.min((page.value + 1) * pageSize, total.value);
|
||||||
// displayedSongs.value = displayedSongs.value.concat(reslist.data.songs);
|
const trackIds = props.listInfo.trackIds.slice(start, end).map((item) => item.id);
|
||||||
displayedSongs.value = JSON.parse(JSON.stringify([...displayedSongs.value, ...reslist.data.songs]));
|
|
||||||
|
if (trackIds.length > 0) {
|
||||||
|
const { data } = await getMusicDetail(trackIds);
|
||||||
|
displayedSongs.value = [...displayedSongs.value, ...data.songs];
|
||||||
page.value++;
|
page.value++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有 trackIds,直接使用 songList 分页
|
||||||
|
const start = page.value * pageSize;
|
||||||
|
const end = Math.min((page.value + 1) * pageSize, props.songList.length);
|
||||||
|
const newSongs = props.songList.slice(start, end);
|
||||||
|
displayedSongs.value = [...displayedSongs.value, ...newSongs];
|
||||||
|
page.value++;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error', error);
|
console.error('加载歌曲失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingMore.value = false;
|
isLoadingMore.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = (e: any) => {
|
// 添加虚拟列表的引用
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
const virtualListRef = ref<VirtualListInst | null>(null);
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 50 && !isLoadingMore.value) {
|
|
||||||
|
// 修改滚动处理函数
|
||||||
|
const handleScroll = (e: Event) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 100 && !isLoadingMore.value) {
|
||||||
loadMoreSongs();
|
loadMoreSongs();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 监听 songList 变化,重置分页状态
|
||||||
watch(
|
watch(
|
||||||
() => songList,
|
() => props.songList,
|
||||||
(newSongs) => {
|
(newSongs) => {
|
||||||
displayedSongs.value = JSON.parse(JSON.stringify(newSongs));
|
page.value = 0;
|
||||||
total.value = listInfo ? listInfo.trackIds.length : displayedSongs.value.length;
|
displayedSongs.value = newSongs.slice(0, pageSize);
|
||||||
|
if (newSongs.length > pageSize) {
|
||||||
|
page.value = 1;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ deep: true, immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 添加计算属性来处理列表高度
|
||||||
|
const listHeight = computed(() => {
|
||||||
|
const baseHeight = '100%'; // 减去标题高度
|
||||||
|
return store.state.isPlay ? `calc(100% - 90px)` : baseHeight; // 112px 是 PlayBottom 的高度
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -168,9 +190,13 @@ watch(
|
|||||||
|
|
||||||
&-list {
|
&-list {
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
|
position: relative; // 添加相对定位
|
||||||
|
|
||||||
&-content {
|
:deep(.n-virtual-list__scroll) {
|
||||||
min-height: 400px;
|
scrollbar-width: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,14 +212,20 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.double-list {
|
.double-list {
|
||||||
@apply flex flex-wrap gap-5;
|
|
||||||
|
|
||||||
.double-item {
|
.double-item {
|
||||||
width: calc(50% - 10px);
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.song-item {
|
.song-item {
|
||||||
background-color: #191919;
|
background-color: #191919;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保 PlayBottom 不会影响滚动区域
|
||||||
|
:deep(.bottom) {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ import directives from './directive';
|
|||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
Object.keys(directives).forEach((key: string) => {
|
Object.keys(directives).forEach((key: string) => {
|
||||||
app.directive(key, directives[key]);
|
app.directive(key, directives[key as keyof typeof directives]);
|
||||||
});
|
});
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(store);
|
app.use(store);
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const handleScroll = (e: any) => {
|
|||||||
// 监听窗口大小变化,调整每行显示数量
|
// 监听窗口大小变化,调整每行显示数量
|
||||||
const updateItemsPerRow = () => {
|
const updateItemsPerRow = () => {
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
if (width > 1800) ITEMS_PER_ROW.value = 10;
|
if (width > 1800) ITEMS_PER_ROW.value = 8;
|
||||||
else if (width > 1200) ITEMS_PER_ROW.value = 8;
|
else if (width > 1200) ITEMS_PER_ROW.value = 8;
|
||||||
else if (width > 768) ITEMS_PER_ROW.value = 6;
|
else if (width > 768) ITEMS_PER_ROW.value = 6;
|
||||||
else ITEMS_PER_ROW.value = 5;
|
else ITEMS_PER_ROW.value = 5;
|
||||||
|
|||||||
@@ -57,12 +57,18 @@ onActivated(() => {
|
|||||||
|
|
||||||
const isShowList = ref(false);
|
const isShowList = ref(false);
|
||||||
const list = ref<Playlist>();
|
const list = ref<Playlist>();
|
||||||
|
const listLoading = ref(false);
|
||||||
// 展示歌单
|
// 展示歌单
|
||||||
const showPlaylist = async (id: number) => {
|
const showPlaylist = async (id: number, name: string) => {
|
||||||
isShowList.value = true;
|
isShowList.value = true;
|
||||||
list.value = {};
|
listLoading.value = true;
|
||||||
|
|
||||||
|
list.value = {
|
||||||
|
name,
|
||||||
|
} as Playlist;
|
||||||
const { data } = await getListDetail(id);
|
const { data } = await getListDetail(id);
|
||||||
list.value = data.playlist;
|
list.value = data.playlist;
|
||||||
|
listLoading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
@@ -103,7 +109,12 @@ const handlePlay = () => {
|
|||||||
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
|
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
|
||||||
<div class="title">创建的歌单</div>
|
<div class="title">创建的歌单</div>
|
||||||
<n-scrollbar>
|
<n-scrollbar>
|
||||||
<div v-for="(item, index) in playList" :key="index" class="play-list-item" @click="showPlaylist(item.id)">
|
<div
|
||||||
|
v-for="(item, index) in playList"
|
||||||
|
:key="index"
|
||||||
|
class="play-list-item"
|
||||||
|
@click="showPlaylist(item.id, item.name)"
|
||||||
|
>
|
||||||
<n-image :src="getImgUrl(item.coverImgUrl, '50y50')" class="play-list-item-img" lazy preview-disabled />
|
<n-image :src="getImgUrl(item.coverImgUrl, '50y50')" class="play-list-item-img" lazy preview-disabled />
|
||||||
<div class="play-list-item-info">
|
<div class="play-list-item-info">
|
||||||
<div class="play-list-item-name">{{ item.name }}</div>
|
<div class="play-list-item-name">{{ item.name }}</div>
|
||||||
@@ -133,7 +144,13 @@ const handlePlay = () => {
|
|||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<music-list v-model:show="isShowList" :name="list?.name || ''" :song-list="list?.tracks || []" :list-info="list" />
|
<music-list
|
||||||
|
v-model:show="isShowList"
|
||||||
|
:name="list?.name || ''"
|
||||||
|
:song-list="list?.tracks || []"
|
||||||
|
:list-info="list"
|
||||||
|
:loading="listLoading"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user