mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-06 06:51:29 +08:00
Feat/virtualizarefactor: virtualization rework — unify Virtual components, fix memory leaks, migrate 15+ consumerstion rework (#472)
This commit is contained in:
@@ -4,7 +4,7 @@ import type { Site, TorrentInfo, SiteCategory } from '@/api/types'
|
||||
import { formatFileSize } from '@core/utils/formatters'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import AddDownloadDialog from '../dialog/AddDownloadDialog.vue'
|
||||
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
|
||||
import VirtualList from '@/components/virtual/VirtualList.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
@@ -471,15 +471,14 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div v-else-if="mobileResourceList.length > 0" class="site-resource-mobile__list px-3 pb-4">
|
||||
<ProgressiveCardGrid
|
||||
<VirtualList
|
||||
:items="mobileResourceList"
|
||||
:columns="1"
|
||||
:gap="12"
|
||||
:estimated-item-height="320"
|
||||
:overscan-rows="5"
|
||||
:estimate-size="320"
|
||||
:overscan="5"
|
||||
:get-item-key="getResourceItemKey"
|
||||
container-height="100%"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<template #item="{ item }">
|
||||
<VCard>
|
||||
<VCardText class="pa-4">
|
||||
<button type="button" class="site-resource-title-btn text-start" @click="addDownload(item)">
|
||||
@@ -578,7 +577,7 @@ onMounted(() => {
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
</ProgressiveCardGrid>
|
||||
</VirtualList>
|
||||
</div>
|
||||
|
||||
<div v-else class="px-4 py-10 text-center text-medium-emphasis">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDisplay } from 'vuetify'
|
||||
import ProgressDialog from './ProgressDialog.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mediaTypeDict } from '@/api/constants'
|
||||
import VirtualList from '@/components/virtual/VirtualList.vue'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -24,9 +25,6 @@ const emit = defineEmits(['close', 'save'])
|
||||
// 订阅历史列表
|
||||
const historyList = ref<Subscribe[]>([])
|
||||
|
||||
// 当前加载数据
|
||||
const currData = ref<Subscribe[]>([])
|
||||
|
||||
// 当前页
|
||||
const currentPage = ref(1)
|
||||
|
||||
@@ -36,6 +34,9 @@ const pageSize = ref(30)
|
||||
// 是否加载中
|
||||
const loading = ref(false)
|
||||
|
||||
// 是否还有更多数据
|
||||
const hasMore = ref(true)
|
||||
|
||||
// 是否加载完成
|
||||
const isRefreshed = ref(false)
|
||||
|
||||
@@ -45,41 +46,31 @@ const progressDialog = ref(false)
|
||||
// 进度文字
|
||||
const progressText = ref('')
|
||||
|
||||
// 调用API查询列表
|
||||
async function loadHistory({ done }: { done: any }) {
|
||||
// 如果正在加载中,直接返回
|
||||
if (loading.value) {
|
||||
done('ok')
|
||||
return
|
||||
}
|
||||
// VirtualList ref(泛型组件无法用 InstanceType 表达,用 any)
|
||||
const listRef = ref<any>(null)
|
||||
|
||||
// 调用API查询列表
|
||||
// 调用API查询列表(VirtualList @load-more 触发)
|
||||
async function loadHistory() {
|
||||
if (loading.value || !hasMore.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
// 设置加载中
|
||||
loading.value = true
|
||||
currData.value = await api.get(`subscribe/history/${props.type}`, {
|
||||
const data: Subscribe[] = await api.get(`subscribe/history/${props.type}`, {
|
||||
params: {
|
||||
page: currentPage.value,
|
||||
count: pageSize.value,
|
||||
},
|
||||
})
|
||||
// 标计为已请求完成
|
||||
isRefreshed.value = true
|
||||
if (currData.value.length === 0) {
|
||||
// 如果没有数据,跳出
|
||||
done('empty')
|
||||
if (!data || data.length === 0) {
|
||||
hasMore.value = false
|
||||
} else {
|
||||
// 合并数据
|
||||
historyList.value = [...historyList.value, ...currData.value]
|
||||
// 页码+1
|
||||
historyList.value.push(...data)
|
||||
currentPage.value++
|
||||
// 返回加载成功
|
||||
done('ok')
|
||||
// 如果服务端返回不足一页,认为没有更多
|
||||
if (data.length < pageSize.value) hasMore.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
// 返回加载失败
|
||||
done('error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -143,6 +134,11 @@ function getMediaTypeText(type: string | undefined) {
|
||||
if (!type) return ''
|
||||
return mediaTypeDict[type]
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
onMounted(() => {
|
||||
void loadHistory()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -154,67 +150,71 @@ function getMediaTypeText(type: string | undefined) {
|
||||
<VDivider />
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VList lines="two" class="flex-grow-1 min-h-0 py-0">
|
||||
<VInfiniteScroll mode="intersect" side="end" :items="historyList" class="h-100" @load="loadHistory">
|
||||
<template #loading>
|
||||
<LoadingBanner />
|
||||
</template>
|
||||
<template #empty />
|
||||
<VVirtualScroll v-if="historyList.length > 0" renderless :items="historyList" :item-height="104">
|
||||
<template #default="{ item, itemRef }">
|
||||
<div :ref="itemRef">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="75"
|
||||
width="50"
|
||||
:src="item.poster"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VImg>
|
||||
</template>
|
||||
<VListItemTitle v-if="item.type == '电视剧'">
|
||||
{{ item.name }}
|
||||
<span class="text-sm">{{ t('dialog.subscribeHistory.season', { season: item.season }) }}</span>
|
||||
</VListItemTitle>
|
||||
<VListItemTitle v-else>
|
||||
{{ item.name }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
|
||||
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
:base-color="menu.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
<VirtualList
|
||||
ref="listRef"
|
||||
:items="historyList"
|
||||
:estimate-size="104"
|
||||
:overscan="6"
|
||||
key-field="id"
|
||||
container-height="60vh"
|
||||
:load-more-threshold="5"
|
||||
@load-more="loadHistory"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<VListItem>
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="75"
|
||||
width="50"
|
||||
:src="item.poster"
|
||||
aspect-ratio="2/3"
|
||||
class="object-cover rounded ring-gray-500 me-3"
|
||||
cover
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="w-full h-full">
|
||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
</div>
|
||||
</template>
|
||||
</VVirtualScroll>
|
||||
</VInfiniteScroll>
|
||||
</VImg>
|
||||
</template>
|
||||
<VListItemTitle v-if="item.type == '电视剧'">
|
||||
{{ item.name }}
|
||||
<span class="text-sm">{{ t('dialog.subscribeHistory.season', { season: item.season }) }}</span>
|
||||
</VListItemTitle>
|
||||
<VListItemTitle v-else>
|
||||
{{ item.name }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="mt-2">{{ formatDateDifference(item.date) }}</VListItemSubtitle>
|
||||
<VListItemSubtitle class="mt-2">{{ item.description }}</VListItemSubtitle>
|
||||
<template #append>
|
||||
<div class="me-n3">
|
||||
<IconBtn>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
<VListItem
|
||||
v-for="(menu, i) in dropdownItems"
|
||||
:key="i"
|
||||
:base-color="menu.color"
|
||||
@click="menu.props.click(item)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon :icon="menu.props.prependIcon" />
|
||||
</template>
|
||||
<VListItemTitle v-text="menu.title" />
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VListItem>
|
||||
</template>
|
||||
<template #loading>
|
||||
<LoadingBanner v-if="loading" />
|
||||
</template>
|
||||
</VirtualList>
|
||||
</VList>
|
||||
<VCardText v-if="historyList.length === 0 && isRefreshed" class="text-center">{{
|
||||
t('dialog.subscribeHistory.noData')
|
||||
|
||||
Reference in New Issue
Block a user