mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
feat(MediaCard): implement lazy loading for API calls
This commit is contained in:
@@ -67,6 +67,12 @@ const sourceIconDict: { [key: string]: any } = {
|
|||||||
bangumi: bangumiImage,
|
bangumi: bangumiImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绑定MediaCard元素
|
||||||
|
const mediaCardRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
// 创建Intersection Observer实例
|
||||||
|
const observer = ref<IntersectionObserver | null>(null)
|
||||||
|
|
||||||
// 获得mediaid
|
// 获得mediaid
|
||||||
function getMediaId() {
|
function getMediaId() {
|
||||||
if (props.media?.tmdb_id) return `tmdb:${props.media?.tmdb_id}`
|
if (props.media?.tmdb_id) return `tmdb:${props.media?.tmdb_id}`
|
||||||
@@ -359,10 +365,40 @@ function handleSearch() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 装载时检查是否已订阅
|
// 懒加载检查
|
||||||
onBeforeMount(() => {
|
function handleCheckLazy() {
|
||||||
handleCheckSubscribe()
|
handleCheckSubscribe()
|
||||||
handleCheckExists()
|
handleCheckExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在元素进入视窗时触发懒加载函数
|
||||||
|
function setupIntersectionObserver() {
|
||||||
|
if (mediaCardRef.value) {
|
||||||
|
observer.value = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
// 只要MediaCard进入视窗,就调用懒加载的操作
|
||||||
|
handleCheckLazy()
|
||||||
|
// 加载后销毁观察者实例
|
||||||
|
observer.value?.disconnect()
|
||||||
|
observer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
)
|
||||||
|
observer.value.observe(mediaCardRef.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupIntersectionObserver()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer.value?.disconnect()
|
||||||
|
observer.value = null
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算图片地址
|
// 计算图片地址
|
||||||
@@ -407,82 +443,84 @@ function onRemoveSubscribe() {
|
|||||||
<template>
|
<template>
|
||||||
<VHover>
|
<VHover>
|
||||||
<template #default="hover">
|
<template #default="hover">
|
||||||
<VCard
|
<div ref="mediaCardRef">
|
||||||
v-bind="hover.props"
|
<VCard
|
||||||
:height="props.height"
|
v-bind="hover.props"
|
||||||
:width="props.width"
|
:height="props.height"
|
||||||
class="outline-none shadow ring-gray-500 rounded-lg"
|
:width="props.width"
|
||||||
:class="{
|
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||||
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
:class="{
|
||||||
'ring-1': isImageLoaded,
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
}"
|
'ring-1': isImageLoaded,
|
||||||
@click.stop="goMediaDetail(hover.isHovering ?? false)"
|
}"
|
||||||
>
|
@click.stop="goMediaDetail(hover.isHovering ?? false)"
|
||||||
<VImg
|
|
||||||
aspect-ratio="2/3"
|
|
||||||
:src="getImgUrl"
|
|
||||||
class="object-cover aspect-w-2 aspect-h-3"
|
|
||||||
cover
|
|
||||||
@load="isImageLoaded = true"
|
|
||||||
@error="imageLoadError = true"
|
|
||||||
>
|
>
|
||||||
<template #placeholder>
|
<VImg
|
||||||
<div class="w-full h-full">
|
aspect-ratio="2/3"
|
||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
:src="getImgUrl"
|
||||||
|
class="object-cover aspect-w-2 aspect-h-3"
|
||||||
|
cover
|
||||||
|
@load="isImageLoaded = true"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VImg>
|
||||||
|
<!-- 详情 -->
|
||||||
|
<VCardText
|
||||||
|
v-show="hover.isHovering || imageLoadError"
|
||||||
|
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
|
||||||
|
>
|
||||||
|
<span class="font-bold">{{ props.media?.year }}</span>
|
||||||
|
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.title }}
|
||||||
|
</h1>
|
||||||
|
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.overview }}
|
||||||
|
</p>
|
||||||
|
<div class="flex align-center justify-between">
|
||||||
|
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
|
||||||
|
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</VCardText>
|
||||||
</VImg>
|
<!-- 类型角标 -->
|
||||||
<!-- 详情 -->
|
<VChip
|
||||||
<VCardText
|
v-show="isImageLoaded"
|
||||||
v-show="hover.isHovering || imageLoadError"
|
variant="elevated"
|
||||||
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
size="small"
|
||||||
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
|
:class="getChipColor(props.media?.type || '')"
|
||||||
>
|
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||||
<span class="font-bold">{{ props.media?.year }}</span>
|
>
|
||||||
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
{{ props.media?.type }}
|
||||||
{{ props.media?.title }}
|
</VChip>
|
||||||
</h1>
|
<!-- 本地存在标识 -->
|
||||||
<p class="leading-4 line-clamp-4 overflow-hidden text-ellipsis ...">
|
<ExistIcon v-if="isExists && !hover.isHovering" />
|
||||||
{{ props.media?.overview }}
|
<!-- 评分角标 -->
|
||||||
</p>
|
<VChip
|
||||||
<div class="flex align-center justify-between">
|
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
|
||||||
<IconBtn icon="mdi-magnify" color="white" @click.stop="handleSearch" />
|
variant="elevated"
|
||||||
<IconBtn icon="mdi-heart" :color="isSubscribed ? 'error' : 'white'" @click.stop="handleSubscribe" />
|
size="small"
|
||||||
</div>
|
:class="getChipColor('rating')"
|
||||||
</VCardText>
|
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||||
<!-- 类型角标 -->
|
>
|
||||||
<VChip
|
{{ props.media?.vote_average }}
|
||||||
v-show="isImageLoaded"
|
</VChip>
|
||||||
variant="elevated"
|
<!--来源图标-->
|
||||||
size="small"
|
<VAvatar
|
||||||
:class="getChipColor(props.media?.type || '')"
|
size="24"
|
||||||
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
density="compact"
|
||||||
>
|
class="absolute bottom-1 right-1"
|
||||||
{{ props.media?.type }}
|
tile
|
||||||
</VChip>
|
v-if="!hover.isHovering && isImageLoaded && props.media?.source"
|
||||||
<!-- 本地存在标识 -->
|
>
|
||||||
<ExistIcon v-if="isExists && !hover.isHovering" />
|
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
|
||||||
<!-- 评分角标 -->
|
</VAvatar>
|
||||||
<VChip
|
</VCard>
|
||||||
v-if="isImageLoaded && props.media?.vote_average && !(isExists && !hover.isHovering)"
|
</div>
|
||||||
variant="elevated"
|
|
||||||
size="small"
|
|
||||||
:class="getChipColor('rating')"
|
|
||||||
class="absolute right-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
|
||||||
>
|
|
||||||
{{ props.media?.vote_average }}
|
|
||||||
</VChip>
|
|
||||||
<!--来源图标-->
|
|
||||||
<VAvatar
|
|
||||||
size="24"
|
|
||||||
density="compact"
|
|
||||||
class="absolute bottom-1 right-1"
|
|
||||||
tile
|
|
||||||
v-if="!hover.isHovering && isImageLoaded && props.media?.source"
|
|
||||||
>
|
|
||||||
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
|
|
||||||
</VAvatar>
|
|
||||||
</VCard>
|
|
||||||
</template>
|
</template>
|
||||||
</VHover>
|
</VHover>
|
||||||
<!-- 订阅季弹窗 -->
|
<!-- 订阅季弹窗 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user