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
+77 -5
View File
@@ -5,10 +5,21 @@
<div> <div>
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name"> <template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
<span <span
v-show="isShowAllPlaylistCategory || index <= 19" v-show="isShowAllPlaylistCategory || index <= 19 || isHiding"
class="play-list-type-item" class="play-list-type-item"
:class="setAnimationClass('animate__bounceIn')" :class="
:style="setAnimationDelay(index <= 19 ? index : index - 19)" setAnimationClass(
index <= 19
? 'animate__bounceIn'
: !isShowAllPlaylistCategory
? 'animate__backOutLeft'
: 'animate__bounceIn',
) +
' ' +
'type-item-' +
index
"
:style="getAnimationDelay(index)"
@click="handleClickPlaylistType(item.name)" @click="handleClickPlaylistType(item.name)"
>{{ item.name }}</span >{{ item.name }}</span
> >
@@ -17,7 +28,7 @@
class="play-list-type-showall" class="play-list-type-showall"
:class="setAnimationClass('animate__bounceIn')" :class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)" :style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory" @click="handleToggleShowAllPlaylistCategory"
> >
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }} {{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
</div> </div>
@@ -26,7 +37,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { getPlaylistCategory } from '@/api/home'; import { getPlaylistCategory } from '@/api/home';
@@ -36,6 +47,59 @@ import { setAnimationClass, setAnimationDelay } from '@/utils';
const playlistCategory = ref<IPlayListSort>(); const playlistCategory = ref<IPlayListSort>();
// 是否显示全部歌单分类 // 是否显示全部歌单分类
const isShowAllPlaylistCategory = ref<boolean>(false); 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 () => { 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(() => { onMounted(() => {
loadPlaylistCategory(); loadPlaylistCategory();
+118 -37
View File
@@ -11,12 +11,31 @@ defineOptions({
name: 'List', 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 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 recommendItem = ref<IRecommendItem | null>();
const listDetail = ref<IListDetail | null>(); const listDetail = ref<IListDetail | null>();
const listLoading = ref(true); const listLoading = ref(true);
const selectRecommendItem = async (item: IRecommendItem) => { const selectRecommendItem = async (item: IRecommendItem) => {
listLoading.value = true; listLoading.value = true;
recommendItem.value = null; recommendItem.value = null;
@@ -32,31 +51,84 @@ const route = useRoute();
const listTitle = ref(route.query.type || '歌单列表'); const listTitle = ref(route.query.type || '歌单列表');
const loading = ref(false); const loading = ref(false);
const loadList = async (type: string) => { const loadList = async (type: string, isLoadMore = false) => {
loading.value = true; if (!hasMore.value && isLoadMore) return;
const params = { if (isLoadMore) {
cat: type || '', isLoadingMore.value = true;
limit: 30, } else {
offset: 0, loading.value = true;
}; page.value = 0;
const { data } = await getListByCat(params); recommendList.value = [];
recommendList.value = data.playlists; }
loading.value = false;
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); const handleScroll = (e: any) => {
} else { const { scrollTop, scrollHeight, clientHeight } = e.target;
getRecommendList().then((res: { data: { result: any } }) => { // 距离底部100px时加载更多
recommendList.value = res.data.result; 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( watch(
() => route.query, () => route.query,
async (newParams) => { async (newParams) => {
if (newParams.type) { if (newParams.type) {
recommendList.value = null; recommendList.value = [];
listTitle.value = newParams.type || '歌单列表'; listTitle.value = newParams.type || '歌单列表';
loadList(newParams.type as string); loadList(newParams.type as string);
} }
@@ -68,14 +140,14 @@ watch(
<div class="list-page"> <div class="list-page">
<div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">{{ listTitle }}</div> <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-loading="loading" class="recommend-list">
<div <div
v-for="(item, index) in recommendList" v-for="(item, index) in recommendList"
:key="item.id" :key="item.id"
class="recommend-item" class="recommend-item"
:class="setAnimationClass('animate__bounceIn')" :class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)" :style="getItemAnimationDelay(index)"
@click.stop="selectRecommendItem(item)" @click.stop="selectRecommendItem(item)"
> >
<div class="recommend-item-img"> <div class="recommend-item-img">
@@ -95,6 +167,12 @@ watch(
<div class="recommend-item-title">{{ item.name }}</div> <div class="recommend-item-title">{{ item.name }}</div>
</div> </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> </n-scrollbar>
<music-list <music-list
v-model:show="showMusic" v-model:show="showMusic"
@@ -108,28 +186,32 @@ watch(
<style lang="scss" scoped> <style lang="scss" scoped>
.list-page { .list-page {
@apply relative h-full w-full px-4; @apply relative h-full w-full;
} }
.recommend { .recommend {
@apply w-full h-full bg-none; @apply w-full h-full bg-none px-4;
&-title { &-title {
@apply text-lg font-bold text-white pb-4; @apply text-lg font-bold text-white pb-4;
} }
&-list { &-list {
@apply grid gap-6 pb-28; @apply grid gap-x-8 gap-y-6 pb-28;
grid-template-columns: repeat(auto-fill, minmax(13%, 1fr)); grid-template-columns: repeat(v-bind(ITEMS_PER_ROW), minmax(0, 1fr));
} }
&-item { &-item {
@apply flex flex-col;
&-img { &-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 { &:hover img {
@apply hover:scale-110 transition-all duration-300 ease-in-out; @apply hover:scale-110 transition-all duration-300 ease-in-out;
} }
&-img {
@apply h-full w-full rounded-xl overflow-hidden;
}
.top { .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; @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; background-color: #00000088;
@@ -147,10 +229,7 @@ watch(
} }
.play-count { .play-count {
position: absolute; @apply absolute top-2 left-2 text-sm;
top: 10px;
left: 10px;
font-size: 14px;
} }
} }
} }
@@ -160,9 +239,11 @@ watch(
} }
} }
.mobile { .loading-more {
.recommend-list { @apply flex items-center justify-center py-4 text-sm text-gray-400;
grid-template-columns: repeat(auto-fill, minmax(25%, 1fr)); }
}
.no-more {
@apply text-center py-4 text-sm text-gray-500;
} }
</style> </style>
+6 -1
View File
@@ -10,7 +10,7 @@
:key="item.id" :key="item.id"
class="mv-item" class="mv-item"
:class="setAnimationClass('animate__bounceIn')" :class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 10)" :style="getItemAnimationDelay(index)"
> >
<div class="mv-item-img" @click="handleShowMv(item, index)"> <div class="mv-item-img" @click="handleShowMv(item, index)">
<n-image <n-image
@@ -68,6 +68,11 @@ const offset = ref(0);
const limit = ref(30); const limit = ref(30);
const hasMore = ref(true); const hasMore = ref(true);
const getItemAnimationDelay = (index: number) => {
const currentPageIndex = index % limit.value;
return setAnimationDelay(currentPageIndex, 30);
};
onMounted(async () => { onMounted(async () => {
await loadMvList(); await loadMvList();
}); });