fix: replace virtual card grid with progressive loading

This commit is contained in:
jxxghp
2026-05-09 22:23:45 +08:00
parent 5909d2423c
commit 2f1a356e65
13 changed files with 289 additions and 326 deletions

View File

@@ -0,0 +1,252 @@
<script setup lang="ts">
import { useIntersectionObserver } from '@vueuse/core'
const props = withDefaults(
defineProps<{
items: any[]
minItemWidth?: number
itemAspectRatio?: number
estimatedItemHeight?: number
scrollToIndex?: number
gap?: number
initialCount?: number
batchSize?: number
overscanRows?: number
getItemKey?: (item: any, index: number) => string | number
}>(),
{
minItemWidth: 144,
itemAspectRatio: 1.5,
estimatedItemHeight: undefined,
scrollToIndex: undefined,
gap: 16,
initialCount: 24,
batchSize: 24,
overscanRows: 4,
getItemKey: undefined,
},
)
const containerRef = ref<HTMLElement | null>(null)
const sentinelRef = ref<HTMLElement | null>(null)
const renderedCount = ref(0)
let animationFrameId: number | null = null
const safeInitialCount = computed(() => Math.max(1, props.initialCount))
const safeBatchSize = computed(() => Math.max(1, props.batchSize))
const hasMoreItems = computed(() => renderedCount.value < props.items.length)
const visibleItems = computed(() => props.items.slice(0, renderedCount.value))
const gridStyle = computed(() => ({
columnGap: `${props.gap}px`,
gridTemplateColumns: `repeat(auto-fill, minmax(${props.minItemWidth}px, 1fr))`,
rowGap: `${props.gap}px`,
}))
function getComparableKey(item: any, index: number) {
if (props.getItemKey) {
return props.getItemKey(item, index)
}
return index
}
function resolveItemKey(item: any, index: number) {
return getComparableKey(item, index)
}
function appendNextBatch() {
renderedCount.value = Math.min(props.items.length, renderedCount.value + safeBatchSize.value)
}
function hasPageScroll() {
if (typeof window === 'undefined') {
return true
}
const scrollHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
return scrollHeight - (window.innerHeight || document.documentElement.clientHeight) > 2
}
async function fillViewport() {
if (typeof window === 'undefined') {
return
}
const maxIterations = Math.ceil(props.items.length / safeBatchSize.value)
let iterations = 0
while (!hasPageScroll() && hasMoreItems.value && iterations < maxIterations) {
appendNextBatch()
iterations += 1
await nextTick()
}
}
function queueFillViewport() {
if (typeof window === 'undefined' || animationFrameId !== null) {
return
}
animationFrameId = window.requestAnimationFrame(() => {
animationFrameId = null
void fillViewport()
})
}
async function revealItem(index: number) {
if (typeof window === 'undefined' || index < 0 || index >= props.items.length) {
return
}
const minRenderedCount = Math.ceil((index + 1) / safeBatchSize.value) * safeBatchSize.value
renderedCount.value = Math.min(props.items.length, Math.max(renderedCount.value, minRenderedCount))
await nextTick()
const target = containerRef.value?.querySelector(`[data-progressive-grid-index="${index}"]`)
if (target instanceof HTMLElement) {
target.scrollIntoView({
behavior: 'auto',
block: 'start',
inline: 'nearest',
})
}
}
function resetVisibleItems() {
renderedCount.value = Math.min(props.items.length, safeInitialCount.value)
nextTick(() => {
if (props.scrollToIndex !== undefined && props.scrollToIndex >= 0) {
void revealItem(props.scrollToIndex)
return
}
queueFillViewport()
})
}
function didItemsAppend(nextItems: any[], previousItems: any[]) {
if (!previousItems.length || nextItems.length < previousItems.length) {
return false
}
return previousItems.every((item, index) => getComparableKey(item, index) === getComparableKey(nextItems[index], index))
}
function syncVisibleItems(nextItems: any[], previousItems: any[] = []) {
if (didItemsAppend(nextItems, previousItems)) {
renderedCount.value = Math.min(nextItems.length, Math.max(renderedCount.value, previousItems.length))
nextTick(() => {
if (props.scrollToIndex !== undefined && props.scrollToIndex >= 0) {
void revealItem(props.scrollToIndex)
return
}
queueFillViewport()
})
return
}
resetVisibleItems()
}
const { stop } = useIntersectionObserver(
sentinelRef,
([entry]) => {
if (!entry?.isIntersecting || !hasMoreItems.value) {
return
}
appendNextBatch()
queueFillViewport()
},
{
rootMargin: '1200px 0px',
},
)
onMounted(() => {
window.addEventListener('resize', queueFillViewport, { passive: true })
})
onUnmounted(() => {
stop()
window.removeEventListener('resize', queueFillViewport)
if (animationFrameId !== null) {
window.cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
})
watch(
[
() => props.minItemWidth,
() => props.initialCount,
() => props.batchSize,
],
() => {
queueFillViewport()
},
{ immediate: true },
)
watch(
() => props.items,
(nextItems, previousItems) => {
syncVisibleItems(nextItems, previousItems)
},
{ immediate: true },
)
watch(
[() => props.scrollToIndex, () => props.items.length],
([scrollToIndex]) => {
if (scrollToIndex === undefined || scrollToIndex < 0) {
return
}
nextTick(() => {
void revealItem(scrollToIndex)
})
},
{ immediate: true },
)
</script>
<template>
<div ref="containerRef" class="progressive-card-grid">
<div class="grid" :style="gridStyle">
<div
v-for="(item, index) in visibleItems"
:key="resolveItemKey(item, index)"
class="progressive-card-grid__item"
:data-progressive-grid-index="index"
>
<slot :item="item" :index="index" />
</div>
</div>
<div v-if="hasMoreItems" ref="sentinelRef" class="progressive-card-grid__sentinel" aria-hidden="true" />
</div>
</template>
<style scoped>
.progressive-card-grid {
inline-size: 100%;
}
.progressive-card-grid__item {
min-inline-size: 0;
}
.progressive-card-grid__sentinel {
block-size: 1px;
inline-size: 100%;
}
</style>

View File

@@ -1,285 +0,0 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
items: any[]
minItemWidth?: number
itemAspectRatio?: number
estimatedItemHeight?: number
scrollToIndex?: number
gap?: number
overscanRows?: number
getItemKey?: (item: any, index: number) => string | number
}>(),
{
minItemWidth: 144,
itemAspectRatio: 1.5,
estimatedItemHeight: undefined,
scrollToIndex: undefined,
gap: 16,
overscanRows: 4,
getItemKey: undefined,
},
)
const containerRef = ref<HTMLElement | null>(null)
const gridRef = ref<HTMLElement | null>(null)
const columnCount = ref(1)
const itemWidth = ref(props.minItemWidth)
const itemHeight = ref(props.minItemWidth * props.itemAspectRatio)
const measuredItemHeight = ref(0)
const startIndex = ref(0)
const endIndex = ref(0)
const layoutSignature = ref('')
let resizeObserver: ResizeObserver | null = null
let animationFrameId: number | null = null
const baseItemHeight = computed(() => props.estimatedItemHeight ?? itemHeight.value)
const effectiveItemHeight = computed(() => Math.max(baseItemHeight.value, measuredItemHeight.value))
const rowStep = computed(() => effectiveItemHeight.value + props.gap)
const totalRows = computed(() => Math.ceil(props.items.length / columnCount.value))
const visibleItems = computed(() => props.items.slice(startIndex.value, endIndex.value))
const renderedRowCount = computed(() => {
if (!visibleItems.value.length) {
return 0
}
return Math.ceil(visibleItems.value.length / columnCount.value)
})
const totalContentHeight = computed(() => {
if (!totalRows.value) {
return 0
}
return totalRows.value * rowStep.value - props.gap
})
const topPadding = computed(() => {
if (!startIndex.value) {
return 0
}
return Math.floor(startIndex.value / columnCount.value) * rowStep.value
})
const renderedHeight = computed(() => {
if (!renderedRowCount.value) {
return 0
}
return renderedRowCount.value * rowStep.value - props.gap
})
const bottomPadding = computed(() => {
return Math.max(totalContentHeight.value - topPadding.value - renderedHeight.value, 0)
})
const gridStyle = computed(() => ({
columnGap: `${props.gap}px`,
gridTemplateColumns: `repeat(${columnCount.value}, minmax(0, 1fr))`,
paddingBottom: `${bottomPadding.value}px`,
paddingTop: `${topPadding.value}px`,
rowGap: `${props.gap}px`,
}))
function resolveItemKey(item: any, index: number) {
if (props.getItemKey) {
return props.getItemKey(item, startIndex.value + index)
}
return startIndex.value + index
}
function updateMeasuredItemHeight() {
const grid = gridRef.value
if (!grid || !grid.childElementCount) {
return
}
const nextSignature = `${columnCount.value}:${Math.round(itemWidth.value)}`
const childHeights = Array.from(grid.children).reduce((maxHeight, child) => {
if (!(child instanceof HTMLElement)) {
return maxHeight
}
return Math.max(maxHeight, Math.ceil(child.getBoundingClientRect().height))
}, 0)
if (!childHeights) {
return
}
if (layoutSignature.value !== nextSignature) {
layoutSignature.value = nextSignature
measuredItemHeight.value = childHeights
queueSyncVisibleRange()
return
}
if (childHeights > measuredItemHeight.value) {
measuredItemHeight.value = childHeights
queueSyncVisibleRange()
}
}
function syncVisibleRange() {
if (typeof window === 'undefined') {
return
}
const container = containerRef.value
if (!container || props.items.length === 0) {
startIndex.value = 0
endIndex.value = 0
return
}
const containerWidth = container.clientWidth
if (!containerWidth) {
return
}
const columns = Math.max(1, Math.floor((containerWidth + props.gap) / (props.minItemWidth + props.gap)))
columnCount.value = columns
itemWidth.value = (containerWidth - props.gap * (columns - 1)) / columns
itemHeight.value = props.estimatedItemHeight ?? itemWidth.value * props.itemAspectRatio
const rowHeight = rowStep.value || 1
const containerTop = window.scrollY + container.getBoundingClientRect().top
const viewportTop = window.scrollY - containerTop
const viewportBottom = viewportTop + window.innerHeight
const startRow = Math.max(0, Math.floor(viewportTop / rowHeight) - props.overscanRows)
const endRow = Math.min(totalRows.value, Math.ceil(viewportBottom / rowHeight) + props.overscanRows)
const endRowExclusive = Math.max(startRow + 1, endRow)
startIndex.value = Math.min(props.items.length, startRow * columns)
endIndex.value = Math.min(props.items.length, endRowExclusive * columns)
}
function queueSyncVisibleRange() {
if (typeof window === 'undefined' || animationFrameId !== null) {
return
}
animationFrameId = window.requestAnimationFrame(() => {
animationFrameId = null
syncVisibleRange()
void nextTick(() => {
updateMeasuredItemHeight()
})
})
}
function scrollToItemIndex(index: number) {
if (typeof window === 'undefined') {
return
}
const container = containerRef.value
if (!container || props.items.length === 0 || index < 0) {
return
}
syncVisibleRange()
const safeIndex = Math.min(index, props.items.length - 1)
const targetRow = Math.floor(safeIndex / columnCount.value)
const targetTop = window.scrollY + container.getBoundingClientRect().top + targetRow * rowStep.value
window.scrollTo({
top: Math.max(targetTop - props.gap, 0),
behavior: 'auto',
})
queueSyncVisibleRange()
}
onMounted(() => {
queueSyncVisibleRange()
window.addEventListener('scroll', queueSyncVisibleRange, { passive: true })
resizeObserver = new ResizeObserver(() => {
queueSyncVisibleRange()
})
if (containerRef.value) {
resizeObserver.observe(containerRef.value)
}
if (gridRef.value) {
resizeObserver.observe(gridRef.value)
}
})
onUnmounted(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('scroll', queueSyncVisibleRange)
if (animationFrameId !== null) {
window.cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
}
resizeObserver?.disconnect()
resizeObserver = null
})
watch(
[
() => props.items.length,
() => props.minItemWidth,
() => props.itemAspectRatio,
() => props.estimatedItemHeight,
() => props.gap,
],
() => {
nextTick(() => {
queueSyncVisibleRange()
})
},
{ immediate: true },
)
watch(
() => columnCount.value,
() => {
layoutSignature.value = ''
measuredItemHeight.value = 0
},
)
watch(
[() => props.scrollToIndex, () => props.items.length],
([scrollToIndex]) => {
if (scrollToIndex === undefined || scrollToIndex < 0) {
return
}
nextTick(() => {
scrollToItemIndex(scrollToIndex)
})
},
{ immediate: true },
)
</script>
<template>
<div ref="containerRef" class="virtual-card-grid">
<div ref="gridRef" class="grid" :style="gridStyle">
<template v-for="(item, index) in visibleItems" :key="resolveItemKey(item, index)">
<slot :item="item" :index="startIndex + index" />
</template>
</div>
</div>
</template>
<style scoped>
.virtual-card-grid {
inline-size: 100%;
}
</style>

View File

@@ -5,7 +5,7 @@ import api from '@/api'
import type { Context } from '@/api/types'
import TorrentCard from '@/components/cards/TorrentCard.vue'
import TorrentItem from '@/components/cards/TorrentItem.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import TorrentFilterBar from '@/components/filter/TorrentFilterBar.vue'
import { useI18n } from 'vue-i18n'
import { useGlobalSettingsStore } from '@/stores/global'
@@ -1051,7 +1051,7 @@ onUnmounted(() => {
class="stream-result-item"
/>
</div>
<VirtualCardGrid
<ProgressiveCardGrid
v-else-if="filteredCardDataList.length > 0"
:items="filteredCardDataList"
:get-item-key="getTorrentItemKey"
@@ -1061,7 +1061,7 @@ onUnmounted(() => {
<template #default="{ item }">
<TorrentCard :torrent="item" :more="item.more" />
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<!-- 无结果时显示 -->
<div v-if="!progressActive && filteredCardDataList.length === 0" class="no-results">
<VIcon icon="mdi-file-search-outline" size="64" color="grey-lighten-1" />

View File

@@ -2,7 +2,7 @@
import api from '@/api'
import type { MediaInfo } from '@/api/types'
import MediaCard from '@/components/cards/MediaCard.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import { useI18n } from 'vue-i18n'
@@ -154,7 +154,7 @@ async function fetchData({ done }: { done: any }) {
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible pt-3 px-2" @load="fetchData">
<template #loading />
<template #empty />
<VirtualCardGrid
<ProgressiveCardGrid
v-if="dataList.length > 0"
:items="dataList"
:get-item-key="item => item.tmdb_id || item.douban_id || item.bangumi_id || item.media_id || item.title"
@@ -163,7 +163,7 @@ async function fetchData({ done }: { done: any }) {
<template #default="{ item }">
<MediaCard :media="item" />
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"

View File

@@ -2,7 +2,7 @@
import api from '@/api'
import type { Person } from '@/api/types'
import PersonCard from '@/components/cards/PersonCard.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import { useI18n } from 'vue-i18n'
@@ -123,11 +123,11 @@ async function fetchData({ done }: { done: any }) {
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-3" @load="fetchData">
<template #loading />
<template #empty />
<VirtualCardGrid v-if="dataList.length > 0" :items="dataList" :get-item-key="item => item.id" tabindex="0">
<ProgressiveCardGrid v-if="dataList.length > 0" :items="dataList" :get-item-key="item => item.id" tabindex="0">
<template #default="{ item }">
<PersonCard :person="item" />
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"

View File

@@ -13,7 +13,7 @@ import PluginMarketSettingDialog from '@/components/dialog/PluginMarketSettingDi
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
import PluginMixedSortCard from '@/components/cards/PluginMixedSortCard.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { usePWA } from '@/composables/usePWA'
import { useDynamicHeaderTab } from '@/composables/useDynamicHeaderTab'
@@ -1566,12 +1566,11 @@ function onDragStartPlugin(evt: any) {
/>
</template>
</draggable>
<VirtualCardGrid
<ProgressiveCardGrid
v-else-if="shouldVirtualizeInstalledMainList"
:items="mixedSortList"
:get-item-key="item => `${item.type}:${item.id}`"
:min-item-width="256"
:estimated-item-height="260"
:scroll-to-index="installedScrollToIndex"
>
<template #default="{ item }">
@@ -1593,7 +1592,7 @@ function onDragStartPlugin(evt: any) {
@drop-to-folder="(event, folderName) => handleDropToFolder(event, folderName)"
/>
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
</template>
<template v-else>
@@ -1625,12 +1624,11 @@ function onDragStartPlugin(evt: any) {
/>
</template>
</draggable>
<VirtualCardGrid
<ProgressiveCardGrid
v-else-if="shouldVirtualizeInstalledFolderList"
:items="draggableFolderPlugins"
:get-item-key="item => item.id"
:min-item-width="256"
:estimated-item-height="260"
>
<template #default="{ item }">
<PluginMixedSortCard
@@ -1648,7 +1646,7 @@ function onDragStartPlugin(evt: any) {
@remove-from-folder="removeFromFolder"
/>
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
</template>
</div>
@@ -1679,7 +1677,7 @@ function onDragStartPlugin(evt: any) {
>
<template #loading />
<template #empty />
<VirtualCardGrid
<ProgressiveCardGrid
v-if="displayUninstalledList.length > 0"
:items="displayUninstalledList"
:get-item-key="item => `${item.id}_v${item.plugin_version}`"
@@ -1689,7 +1687,7 @@ function onDragStartPlugin(evt: any) {
<template #default="{ item }">
<PluginAppCard :plugin="item" :count="PluginStatistics[item.id || '0']" @install="pluginInstalled" />
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
</VInfiniteScroll>
<NoDataFound
v-if="displayUninstalledList.length === 0 && isAppMarketLoaded"

View File

@@ -4,7 +4,7 @@ import api from '@/api'
import type { DownloadingInfo } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import DownloadingCard from '@/components/cards/DownloadingCard.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
@@ -68,7 +68,7 @@ const { loading: dataLoading } = useDataRefresh(
<template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VPullToRefresh v-model="loading" @load="onRefresh" :pull-down-threshold="64">
<VirtualCardGrid
<ProgressiveCardGrid
v-if="filteredDataList.length > 0"
:items="filteredDataList"
:get-item-key="item => item.hash || item.name"
@@ -78,7 +78,7 @@ const { loading: dataLoading } = useDataRefresh(
<template #default="{ item }">
<DownloadingCard :info="item" :downloader-name="props.name" />
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<NoDataFound
v-if="filteredDataList.length === 0 && isRefreshed"
error-code="404"

View File

@@ -7,7 +7,7 @@ import NoDataFound from '@/components/NoDataFound.vue'
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
import SiteStatisticsDialog from '@/components/dialog/SiteStatisticsDialog.vue'
import SiteImportDialog from '@/components/dialog/SiteImportDialog.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useDynamicButton } from '@/composables/useDynamicButton'
import { useI18n } from 'vue-i18n'
import { usePWA } from '@/composables/usePWA'
@@ -422,12 +422,11 @@ useDynamicButton({
/>
</template>
</draggable>
<VirtualCardGrid
<ProgressiveCardGrid
v-else-if="draggableSiteList.length > 0 && shouldVirtualizeList"
:items="draggableSiteList"
:get-item-key="item => item.id"
:min-item-width="256"
:estimated-item-height="240"
class="px-2"
>
<template #default="{ item }">
@@ -441,7 +440,7 @@ useDynamicButton({
@refresh-stats="handleRefreshStats"
/>
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
</div>
<NoDataFound
v-if="draggableSiteList.length === 0 && isRefreshed"

View File

@@ -5,7 +5,7 @@ import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useUserStore } from '@/stores'
import { useI18n } from 'vue-i18n'
import { useToast } from 'vue-toastification'
@@ -513,12 +513,11 @@ defineExpose({
/>
</template>
</draggable>
<VirtualCardGrid
<ProgressiveCardGrid
v-else-if="displayList.length > 0 && shouldVirtualizeList"
:items="displayList"
:get-item-key="item => item.id"
:min-item-width="240"
:estimated-item-height="300"
:scroll-to-index="scrollToIndex"
class="px-2"
>
@@ -534,7 +533,7 @@ defineExpose({
@select="toggleSelectSubscribe(item.id)"
/>
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<NoDataFound
v-if="displayList.length === 0 && isRefreshed"
error-code="404"

View File

@@ -3,7 +3,7 @@ import api from '@/api'
import type { MediaInfo } from '@/api/types'
import MediaCard from '@/components/cards/MediaCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useI18n } from 'vue-i18n'
// 国际化
@@ -275,7 +275,7 @@ async function fetchData({ done }: { done: any }) {
>
<template #loading />
<template #empty />
<VirtualCardGrid
<ProgressiveCardGrid
v-if="dataList.length > 0"
:items="dataList"
:get-item-key="item => item.tmdb_id || item.douban_id || item.bangumi_id || item.media_id || item.title"
@@ -292,7 +292,7 @@ async function fetchData({ done }: { done: any }) {
</div>
</div>
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"

View File

@@ -3,7 +3,7 @@ import api from '@/api'
import type { SubscribeShare } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import SubscribeShareCard from '@/components/cards/SubscribeShareCard.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useI18n } from 'vue-i18n'
// 国际化
@@ -295,7 +295,7 @@ function removeData(id: number) {
>
<template #loading />
<template #empty />
<VirtualCardGrid
<ProgressiveCardGrid
v-if="dataList.length > 0"
:items="dataList"
:get-item-key="item => item.id || `${item.tmdbid || item.doubanid || item.name}-${item.share_user}`"
@@ -306,7 +306,7 @@ function removeData(id: number) {
<template #default="{ item }">
<SubscribeShareCard :media="item" @delete="removeData(item.id || 0)" />
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"

View File

@@ -4,7 +4,7 @@ import { Workflow } from '@/api/types'
import WorkflowAddEditDialog from '@/components/dialog/WorkflowAddEditDialog.vue'
import WorkflowTaskCard from '@/components/cards/WorkflowTaskCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useI18n } from 'vue-i18n'
// 国际化
@@ -67,7 +67,7 @@ defineExpose({
<template>
<div>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VirtualCardGrid
<ProgressiveCardGrid
v-if="workflowList.length > 0 && isRefreshed"
:items="workflowList"
:get-item-key="item => item.id"
@@ -78,7 +78,7 @@ defineExpose({
<template #default="{ item }">
<WorkflowTaskCard :workflow="item" :event-types="eventTypes" @refresh="fetchData" />
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<NoDataFound
v-if="workflowList.length === 0 && isRefreshed"
error-code="404"

View File

@@ -3,7 +3,7 @@ import api from '@/api'
import type { WorkflowShare } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
import WorkflowShareCard from '@/components/cards/WorkflowShareCard.vue'
import VirtualCardGrid from '@/components/misc/VirtualCardGrid.vue'
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
import { useI18n } from 'vue-i18n'
// 国际化
@@ -157,7 +157,7 @@ onActivated(() => {
<VInfiniteScroll mode="intersect" side="end" :items="dataList" class="overflow-visible px-2" @load="fetchData" :key="currentKey">
<template #loading />
<template #empty />
<VirtualCardGrid
<ProgressiveCardGrid
v-if="dataList.length > 0"
:items="dataList"
:get-item-key="item => item.id"
@@ -173,7 +173,7 @@ onActivated(() => {
@update="emit('update')"
/>
</template>
</VirtualCardGrid>
</ProgressiveCardGrid>
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"