From 06355ff91d5b2e895433d2df5cf57f421382caa8 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 15 May 2026 18:27:56 +0800 Subject: [PATCH] fix: prevent event propagation on card menu buttons and implement virtualization locking for overlays in ProgressiveCardGrid --- package.json | 2 +- src/components/cards/PluginAppCard.vue | 2 +- src/components/cards/PluginCard.vue | 2 +- src/components/cards/SiteCard.vue | 2 +- src/components/cards/SubscribeCard.vue | 2 +- src/components/misc/ProgressiveCardGrid.vue | 108 +++++++++++++++++++- 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 32547ff7..e128f639 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moviepilot", - "version": "2.11.3", + "version": "2.11.4", "private": true, "type": "module", "bin": "dist/service.js", diff --git a/src/components/cards/PluginAppCard.vue b/src/components/cards/PluginAppCard.vue index 8c3282dc..703ed89b 100644 --- a/src/components/cards/PluginAppCard.vue +++ b/src/components/cards/PluginAppCard.vue @@ -252,7 +252,7 @@ const dropdownItems = ref([
- + diff --git a/src/components/cards/PluginCard.vue b/src/components/cards/PluginCard.vue index cf2c092d..dbda2066 100644 --- a/src/components/cards/PluginCard.vue +++ b/src/components/cards/PluginCard.vue @@ -521,7 +521,7 @@ watch(
- + diff --git a/src/components/cards/SiteCard.vue b/src/components/cards/SiteCard.vue index 790cec35..8dd54f1d 100644 --- a/src/components/cards/SiteCard.vue +++ b/src/components/cards/SiteCard.vue @@ -386,7 +386,7 @@ onMounted(() => { - + diff --git a/src/components/cards/SubscribeCard.vue b/src/components/cards/SubscribeCard.vue index f2748c50..3603072b 100644 --- a/src/components/cards/SubscribeCard.vue +++ b/src/components/cards/SubscribeCard.vue @@ -372,7 +372,7 @@ function handleCardClick() { :ripple="!props.batchMode && !props.sortable" >
- + diff --git a/src/components/misc/ProgressiveCardGrid.vue b/src/components/misc/ProgressiveCardGrid.vue index 4a7e2004..d2ec05ec 100644 --- a/src/components/misc/ProgressiveCardGrid.vue +++ b/src/components/misc/ProgressiveCardGrid.vue @@ -38,6 +38,13 @@ interface VirtualCell { key: ItemKey } +interface VirtualRange { + endIndex: number + endRow: number + startIndex: number + startRow: number +} + const containerRef = ref(null) const trackRef = ref(null) @@ -45,6 +52,8 @@ const layoutWidth = ref(0) const viewportTop = ref(0) const viewportBottom = ref(0) const heightVersion = ref(0) +const frozenVisibleRange = ref(null) +const isOverlayGrid = ref(false) const itemHeights = new Map() const observedElements = new Map() @@ -53,6 +62,7 @@ const itemRefCallbacks = new Map { const totalHeight = computed(() => rowMetrics.value.totalHeight) -const visibleRange = computed(() => { +const calculatedVisibleRange = computed(() => { + if (isOverlayGrid.value) { + const rowCount = Math.max(1, Math.ceil(props.items.length / columnCount.value)) + + return { + endIndex: props.items.length, + endRow: rowCount - 1, + startIndex: 0, + startRow: 0, + } + } + const { heights, offsets, rowCount } = rowMetrics.value if (!props.items.length || rowCount === 0) { @@ -176,6 +197,8 @@ const visibleRange = computed(() => { } }) +const visibleRange = computed(() => frozenVisibleRange.value ?? calculatedVisibleRange.value) + const visibleCells = computed(() => { const cells: VirtualCell[] = [] @@ -190,7 +213,13 @@ const visibleCells = computed(() => { return cells }) -const topSpacerHeight = computed(() => rowMetrics.value.offsets[visibleRange.value.startRow] ?? 0) +const topSpacerHeight = computed(() => { + if (isOverlayGrid.value) { + return 0 + } + + return rowMetrics.value.offsets[visibleRange.value.startRow] ?? 0 +}) const visibleBlockHeight = computed(() => { if (!props.items.length || visibleRange.value.endIndex <= visibleRange.value.startIndex) { @@ -206,6 +235,10 @@ const visibleBlockHeight = computed(() => { }) const bottomSpacerHeight = computed(() => { + if (isOverlayGrid.value) { + return 0 + } + return Math.max(totalHeight.value - topSpacerHeight.value - visibleBlockHeight.value, 0) }) @@ -266,6 +299,45 @@ function findLastRowAtOrBeforeOffset(offsets: number[], rowCount: number, offset return answer } +function isDocumentOverlayLocked() { + return typeof document !== 'undefined' && document.documentElement.classList.contains('v-overlay-scroll-blocked') +} + +function isGridInsideOverlay() { + return Boolean(containerRef.value?.closest('.v-overlay, .v-overlay__content')) +} + +function syncOverlayGridState() { + isOverlayGrid.value = isGridInsideOverlay() +} + +function shouldPauseVirtualSync() { + return isDocumentOverlayLocked() && !isOverlayGrid.value +} + +function freezeVisibleRange() { + if (frozenVisibleRange.value) { + return + } + + // 弹窗打开期间固定当前渲染窗口,防止 body 锁滚动造成坐标跳变并卸载触发弹窗的卡片。 + frozenVisibleRange.value = { ...calculatedVisibleRange.value } +} + +function releaseVisibleRange() { + frozenVisibleRange.value = null +} + +function handleOverlayLockChange() { + if (shouldPauseVirtualSync()) { + freezeVisibleRange() + return + } + + releaseVisibleRange() + queueLayoutSync() +} + function getElementFromRef(element: Element | ComponentPublicInstance | null): HTMLElement | null { if (!element || typeof HTMLElement === 'undefined') { return null @@ -312,6 +384,11 @@ function ensureItemResizeObserver() { } itemResizeObserver = new ResizeObserver(entries => { + if (shouldPauseVirtualSync()) { + freezeVisibleRange() + return + } + let shouldUpdate = false let scrollAdjustment = 0 const currentViewportTop = viewportTop.value @@ -506,6 +583,15 @@ function queueLayoutSync() { layoutFrameId = window.requestAnimationFrame(() => { layoutFrameId = null + + if (shouldPauseVirtualSync()) { + freezeVisibleRange() + return + } + + // 弹窗内容已经由 overlay 限定生命周期,直接完整渲染可避免弹窗内交互被虚拟回收打断。 + syncOverlayGridState() + releaseVisibleRange() syncLayoutWidth() refreshScrollTarget() syncViewport() @@ -520,6 +606,13 @@ function queueViewportSync() { scrollFrameId = window.requestAnimationFrame(() => { scrollFrameId = null + + if (shouldPauseVirtualSync()) { + freezeVisibleRange() + return + } + + releaseVisibleRange() syncViewport() }) } @@ -681,6 +774,7 @@ function invalidateMeasurementsForLayoutChange() { onMounted(() => { mounted = true + syncOverlayGridState() scrollTarget = findScrollTarget() addScrollListener(scrollTarget) @@ -689,6 +783,14 @@ onMounted(() => { resizeObserver.observe(trackRef.value) } + if (typeof MutationObserver !== 'undefined') { + overlayLockObserver = new MutationObserver(handleOverlayLockChange) + overlayLockObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'], + }) + } + window.addEventListener('resize', queueLayoutSync, { passive: true }) queueLayoutSync() @@ -716,6 +818,8 @@ onUnmounted(() => { resizeObserver = null itemResizeObserver?.disconnect() itemResizeObserver = null + overlayLockObserver?.disconnect() + overlayLockObserver = null if (layoutFrameId !== null) { window.cancelAnimationFrame(layoutFrameId)