feat: 优化歌单列表 添加加载更多 优化自动布局 优化歌单 mv 歌单类型的动画效果

This commit is contained in:
alger
2024-11-28 23:33:38 +08:00
parent d925f40303
commit f03372de6a
3 changed files with 201 additions and 43 deletions

View File

@@ -5,10 +5,21 @@
<div>
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
<span
v-show="isShowAllPlaylistCategory || index <= 19"
v-show="isShowAllPlaylistCategory || index <= 19 || isHiding"
class="play-list-type-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
:class="
setAnimationClass(
index <= 19
? 'animate__bounceIn'
: !isShowAllPlaylistCategory
? 'animate__backOutLeft'
: 'animate__bounceIn',
) +
' ' +
'type-item-' +
index
"
:style="getAnimationDelay(index)"
@click="handleClickPlaylistType(item.name)"
>{{ item.name }}</span
>
@@ -17,7 +28,7 @@
class="play-list-type-showall"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
@click="handleToggleShowAllPlaylistCategory"
>
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
</div>
@@ -26,7 +37,7 @@
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { getPlaylistCategory } from '@/api/home';
@@ -36,6 +47,59 @@ import { setAnimationClass, setAnimationDelay } from '@/utils';
const playlistCategory = ref<IPlayListSort>();
// 是否显示全部歌单分类
const isShowAllPlaylistCategory = ref<boolean>(false);
const DELAY_TIME = 40;
const getAnimationDelay = computed(() => {
return (index: number) => {
if (index <= 19) {
return setAnimationDelay(index, DELAY_TIME);
}
if (!isShowAllPlaylistCategory.value) {
const nowIndex = (playlistCategory.value?.sub.length || 0) - index;
return setAnimationDelay(nowIndex, DELAY_TIME);
}
return setAnimationDelay(index - 19, DELAY_TIME);
};
});
watch(isShowAllPlaylistCategory, (newVal) => {
if (!newVal) {
const elements = playlistCategory.value?.sub.map((item, index) =>
document.querySelector(`.type-item-${index}`),
) as HTMLElement[];
elements
.slice(20)
.reverse()
.forEach((element, index) => {
if (element) {
setTimeout(
() => {
(element as HTMLElement).style.position = 'absolute';
},
index * DELAY_TIME + 400,
);
}
});
setTimeout(
() => {
isHiding.value = false;
document.querySelectorAll('.play-list-type-item').forEach((element) => {
if (element) {
console.log('element', element);
(element as HTMLElement).style.position = 'none';
}
});
},
(playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME,
);
} else {
document.querySelectorAll('.play-list-type-item').forEach((element) => {
if (element) {
(element as HTMLElement).style.position = 'none';
}
});
}
});
// 加载歌单分类
const loadPlaylistCategory = async () => {
@@ -52,6 +116,14 @@ const handleClickPlaylistType = (type: string) => {
},
});
};
const isHiding = ref<boolean>(false);
const handleToggleShowAllPlaylistCategory = () => {
isShowAllPlaylistCategory.value = !isShowAllPlaylistCategory.value;
if (!isShowAllPlaylistCategory.value) {
isHiding.value = true;
}
};
// 页面初始化
onMounted(() => {
loadPlaylistCategory();

View File

@@ -11,12 +11,31 @@ defineOptions({
name: 'List',
});
const recommendList = ref();
const ITEMS_PER_ROW = ref(6); // 每行显示的数量
const TOTAL_ITEMS = 30; // 每页数量
// 计算实际需要加载的数量,确保能被每行数量整除
const getAdjustedLimit = (perRow: number) => {
return Math.ceil(TOTAL_ITEMS / perRow) * perRow;
};
const recommendList = ref<any[]>([]);
const showMusic = ref(false);
const page = ref(0);
const hasMore = ref(true);
const isLoadingMore = ref(false);
// 计算每个项目在当前页面中的索引
const getItemAnimationDelay = (index: number) => {
const adjustedLimit = getAdjustedLimit(ITEMS_PER_ROW.value);
const currentPageIndex = index % adjustedLimit;
return setAnimationDelay(currentPageIndex, 30);
};
const recommendItem = ref<IRecommendItem | null>();
const listDetail = ref<IListDetail | null>();
const listLoading = ref(true);
const selectRecommendItem = async (item: IRecommendItem) => {
listLoading.value = true;
recommendItem.value = null;
@@ -32,31 +51,84 @@ const route = useRoute();
const listTitle = ref(route.query.type || '歌单列表');
const loading = ref(false);
const loadList = async (type: string) => {
loading.value = true;
const params = {
cat: type || '',
limit: 30,
offset: 0,
};
const { data } = await getListByCat(params);
recommendList.value = data.playlists;
loading.value = false;
const loadList = async (type: string, isLoadMore = false) => {
if (!hasMore.value && isLoadMore) return;
if (isLoadMore) {
isLoadingMore.value = true;
} else {
loading.value = true;
page.value = 0;
recommendList.value = [];
}
try {
const adjustedLimit = getAdjustedLimit(ITEMS_PER_ROW.value);
const params = {
cat: type || '',
limit: adjustedLimit,
offset: page.value * adjustedLimit,
};
const { data } = await getListByCat(params);
if (isLoadMore) {
recommendList.value.push(...data.playlists);
} else {
recommendList.value = data.playlists;
}
hasMore.value = data.more;
page.value++;
} catch (error) {
console.error('加载歌单列表失败:', error);
} finally {
loading.value = false;
isLoadingMore.value = false;
}
};
if (route.query.type) {
loadList(route.query.type as string);
} else {
getRecommendList().then((res: { data: { result: any } }) => {
recommendList.value = res.data.result;
});
}
// 监听滚动事件
const handleScroll = (e: any) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
// 距离底部100px时加载更多
if (scrollTop + clientHeight >= scrollHeight - 100 && !isLoadingMore.value && hasMore.value) {
loadList(route.query.type as string, true);
}
};
// 监听窗口大小变化,调整每行显示数量
const updateItemsPerRow = () => {
const width = window.innerWidth;
if (width > 1800) ITEMS_PER_ROW.value = 8;
else if (width > 1500) ITEMS_PER_ROW.value = 6;
else if (width > 1200) ITEMS_PER_ROW.value = 5;
else if (width > 768) ITEMS_PER_ROW.value = 4;
else ITEMS_PER_ROW.value = 3;
// 如果已经加载了数据,重新加载以适应新的布局
if (route.query.type && recommendList.value) {
loadList(route.query.type as string);
}
};
onMounted(() => {
updateItemsPerRow();
window.addEventListener('resize', updateItemsPerRow);
if (route.query.type) {
loadList(route.query.type as string);
} else {
getRecommendList(getAdjustedLimit(ITEMS_PER_ROW.value)).then((res: { data: { result: any } }) => {
recommendList.value = res.data.result;
});
}
});
onUnmounted(() => {
window.removeEventListener('resize', updateItemsPerRow);
});
watch(
() => route.query,
async (newParams) => {
if (newParams.type) {
recommendList.value = null;
recommendList.value = [];
listTitle.value = newParams.type || '歌单列表';
loadList(newParams.type as string);
}
@@ -68,14 +140,14 @@ watch(
<div class="list-page">
<div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">{{ listTitle }}</div>
<!-- 歌单列表 -->
<n-scrollbar class="recommend" :size="100" @click="showMusic = false">
<n-scrollbar class="recommend" :size="100" @scroll="handleScroll">
<div v-loading="loading" class="recommend-list">
<div
v-for="(item, index) in recommendList"
:key="item.id"
class="recommend-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
:style="getItemAnimationDelay(index)"
@click.stop="selectRecommendItem(item)"
>
<div class="recommend-item-img">
@@ -95,6 +167,12 @@ watch(
<div class="recommend-item-title">{{ item.name }}</div>
</div>
</div>
<!-- 加载状态 -->
<div v-if="isLoadingMore" class="loading-more">
<n-spin size="small" />
<span class="ml-2">加载中...</span>
</div>
<div v-if="!hasMore && recommendList.length > 0" class="no-more">没有更多了</div>
</n-scrollbar>
<music-list
v-model:show="showMusic"
@@ -108,28 +186,32 @@ watch(
<style lang="scss" scoped>
.list-page {
@apply relative h-full w-full px-4;
@apply relative h-full w-full;
}
.recommend {
@apply w-full h-full bg-none;
@apply w-full h-full bg-none px-4;
&-title {
@apply text-lg font-bold text-white pb-4;
}
&-list {
@apply grid gap-6 pb-28;
grid-template-columns: repeat(auto-fill, minmax(13%, 1fr));
@apply grid gap-x-8 gap-y-6 pb-28;
grid-template-columns: repeat(v-bind(ITEMS_PER_ROW), minmax(0, 1fr));
}
&-item {
@apply flex flex-col;
&-img {
@apply rounded-xl overflow-hidden relative;
@apply rounded-xl overflow-hidden relative w-full;
&-img {
@apply block w-full h-full;
}
img {
@apply absolute top-0 left-0 w-full h-full object-cover rounded-xl;
}
&:hover img {
@apply hover:scale-110 transition-all duration-300 ease-in-out;
}
&-img {
@apply h-full w-full rounded-xl overflow-hidden;
}
.top {
@apply absolute w-full h-full top-0 left-0 flex justify-center items-center transition-all duration-300 ease-in-out cursor-pointer;
background-color: #00000088;
@@ -147,10 +229,7 @@ watch(
}
.play-count {
position: absolute;
top: 10px;
left: 10px;
font-size: 14px;
@apply absolute top-2 left-2 text-sm;
}
}
}
@@ -160,9 +239,11 @@ watch(
}
}
.mobile {
.recommend-list {
grid-template-columns: repeat(auto-fill, minmax(25%, 1fr));
}
.loading-more {
@apply flex items-center justify-center py-4 text-sm text-gray-400;
}
.no-more {
@apply text-center py-4 text-sm text-gray-500;
}
</style>

View File

@@ -10,7 +10,7 @@
:key="item.id"
class="mv-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 10)"
:style="getItemAnimationDelay(index)"
>
<div class="mv-item-img" @click="handleShowMv(item, index)">
<n-image
@@ -68,6 +68,11 @@ const offset = ref(0);
const limit = ref(30);
const hasMore = ref(true);
const getItemAnimationDelay = (index: number) => {
const currentPageIndex = index % limit.value;
return setAnimationDelay(currentPageIndex, 30);
};
onMounted(async () => {
await loadMvList();
});