mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +08:00
fix: prevent event propagation on card menu buttons and implement virtualization locking for overlays in ProgressiveCardGrid
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "moviepilot",
|
||||
"version": "2.11.3",
|
||||
"version": "2.11.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": "dist/service.js",
|
||||
|
||||
@@ -252,7 +252,7 @@ const dropdownItems = ref([
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<IconBtn>
|
||||
<IconBtn @click.stop>
|
||||
<VIcon size="small" icon="mdi-dots-vertical" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
|
||||
@@ -521,7 +521,7 @@ watch(
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!props.sortable" class="absolute bottom-0 right-0">
|
||||
<IconBtn>
|
||||
<IconBtn @click.stop>
|
||||
<VIcon icon="mdi-dots-vertical" />
|
||||
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
|
||||
@@ -386,7 +386,7 @@ onMounted(() => {
|
||||
</VBtn>
|
||||
|
||||
<!-- 更多选项按钮 -->
|
||||
<VBtn icon variant="text" class="mt-auto" size="36">
|
||||
<VBtn icon variant="text" class="mt-auto" size="36" @click.stop>
|
||||
<VIcon icon="mdi-dots-vertical" size="20" />
|
||||
<VMenu :activator="'parent'" :close-on-content-click="true" :location="'left'">
|
||||
<VList>
|
||||
|
||||
@@ -372,7 +372,7 @@ function handleCardClick() {
|
||||
:ripple="!props.batchMode && !props.sortable"
|
||||
>
|
||||
<div v-if="!props.sortable" class="me-n3 absolute top-1 right-4">
|
||||
<IconBtn>
|
||||
<IconBtn @click.stop>
|
||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||
<VMenu activator="parent" close-on-content-click>
|
||||
<VList>
|
||||
|
||||
@@ -38,6 +38,13 @@ interface VirtualCell {
|
||||
key: ItemKey
|
||||
}
|
||||
|
||||
interface VirtualRange {
|
||||
endIndex: number
|
||||
endRow: number
|
||||
startIndex: number
|
||||
startRow: number
|
||||
}
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const trackRef = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -45,6 +52,8 @@ const layoutWidth = ref(0)
|
||||
const viewportTop = ref(0)
|
||||
const viewportBottom = ref(0)
|
||||
const heightVersion = ref(0)
|
||||
const frozenVisibleRange = ref<VirtualRange | null>(null)
|
||||
const isOverlayGrid = ref(false)
|
||||
|
||||
const itemHeights = new Map<ItemKey, number>()
|
||||
const observedElements = new Map<HTMLElement, ItemKey>()
|
||||
@@ -53,6 +62,7 @@ const itemRefCallbacks = new Map<ItemKey, (element: Element | ComponentPublicIns
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let itemResizeObserver: ResizeObserver | null = null
|
||||
let overlayLockObserver: MutationObserver | null = null
|
||||
let scrollTarget: ScrollTarget | null = null
|
||||
let layoutFrameId: number | null = null
|
||||
let scrollFrameId: number | null = null
|
||||
@@ -149,7 +159,18 @@ const rowMetrics = computed(() => {
|
||||
|
||||
const totalHeight = computed(() => rowMetrics.value.totalHeight)
|
||||
|
||||
const visibleRange = computed(() => {
|
||||
const calculatedVisibleRange = computed<VirtualRange>(() => {
|
||||
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<VirtualCell[]>(() => {
|
||||
const cells: VirtualCell[] = []
|
||||
|
||||
@@ -190,7 +213,13 @@ const visibleCells = computed<VirtualCell[]>(() => {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user