mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
fix: stabilize virtual card grid during fast scrolling
This commit is contained in:
@@ -22,16 +22,20 @@ const props = withDefaults(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
|
const gridRef = ref<HTMLElement | null>(null)
|
||||||
const columnCount = ref(1)
|
const columnCount = ref(1)
|
||||||
const itemWidth = ref(props.minItemWidth)
|
const itemWidth = ref(props.minItemWidth)
|
||||||
const itemHeight = ref(props.minItemWidth * props.itemAspectRatio)
|
const itemHeight = ref(props.minItemWidth * props.itemAspectRatio)
|
||||||
|
const measuredItemHeight = ref(0)
|
||||||
const startIndex = ref(0)
|
const startIndex = ref(0)
|
||||||
const endIndex = ref(0)
|
const endIndex = ref(0)
|
||||||
|
const layoutSignature = ref('')
|
||||||
|
|
||||||
let resizeObserver: ResizeObserver | null = null
|
let resizeObserver: ResizeObserver | null = null
|
||||||
let animationFrameId: number | null = null
|
let animationFrameId: number | null = null
|
||||||
|
|
||||||
const effectiveItemHeight = computed(() => props.estimatedItemHeight ?? itemHeight.value)
|
const baseItemHeight = computed(() => props.estimatedItemHeight ?? itemHeight.value)
|
||||||
|
const effectiveItemHeight = computed(() => Math.max(baseItemHeight.value, measuredItemHeight.value))
|
||||||
const rowStep = computed(() => effectiveItemHeight.value + props.gap)
|
const rowStep = computed(() => effectiveItemHeight.value + props.gap)
|
||||||
const totalRows = computed(() => Math.ceil(props.items.length / columnCount.value))
|
const totalRows = computed(() => Math.ceil(props.items.length / columnCount.value))
|
||||||
|
|
||||||
@@ -89,6 +93,38 @@ function resolveItemKey(item: any, index: number) {
|
|||||||
return 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() {
|
function syncVisibleRange() {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return
|
return
|
||||||
@@ -131,6 +167,10 @@ function queueSyncVisibleRange() {
|
|||||||
animationFrameId = window.requestAnimationFrame(() => {
|
animationFrameId = window.requestAnimationFrame(() => {
|
||||||
animationFrameId = null
|
animationFrameId = null
|
||||||
syncVisibleRange()
|
syncVisibleRange()
|
||||||
|
|
||||||
|
void nextTick(() => {
|
||||||
|
updateMeasuredItemHeight()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +209,10 @@ onMounted(() => {
|
|||||||
if (containerRef.value) {
|
if (containerRef.value) {
|
||||||
resizeObserver.observe(containerRef.value)
|
resizeObserver.observe(containerRef.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gridRef.value) {
|
||||||
|
resizeObserver.observe(gridRef.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -201,6 +245,14 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => columnCount.value,
|
||||||
|
() => {
|
||||||
|
layoutSignature.value = ''
|
||||||
|
measuredItemHeight.value = 0
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[() => props.scrollToIndex, () => props.items.length],
|
[() => props.scrollToIndex, () => props.items.length],
|
||||||
([scrollToIndex]) => {
|
([scrollToIndex]) => {
|
||||||
@@ -218,7 +270,7 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="containerRef" class="virtual-card-grid">
|
<div ref="containerRef" class="virtual-card-grid">
|
||||||
<div class="grid" :style="gridStyle">
|
<div ref="gridRef" class="grid" :style="gridStyle">
|
||||||
<template v-for="(item, index) in visibleItems" :key="resolveItemKey(item, index)">
|
<template v-for="(item, index) in visibleItems" :key="resolveItemKey(item, index)">
|
||||||
<slot :item="item" :index="startIndex + index" />
|
<slot :item="item" :index="startIndex + index" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user