Feat/virtualizarefactor: virtualization rework — unify Virtual components, fix memory leaks, migrate 15+ consumerstion rework (#472)

This commit is contained in:
Aqr-K
2026-05-15 21:15:30 +08:00
committed by GitHub
parent 0fda7c70de
commit 5953496d84
51 changed files with 2398 additions and 2130 deletions

View File

@@ -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">

View File

@@ -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')