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