mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
✨ feat: 优化歌单列表 添加加载更多 优化自动布局 优化歌单 mv 歌单类型的动画效果
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user