mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-09 14:31:35 +08:00
fix: prevent event propagation on card menu buttons and implement virtualization locking for overlays in ProgressiveCardGrid
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "2.11.3",
|
"version": "2.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ const dropdownItems = ref([
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 right-0">
|
<div class="absolute bottom-0 right-0">
|
||||||
<IconBtn>
|
<IconBtn @click.stop>
|
||||||
<VIcon size="small" icon="mdi-dots-vertical" />
|
<VIcon size="small" icon="mdi-dots-vertical" />
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<VList>
|
<VList>
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ watch(
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!props.sortable" class="absolute bottom-0 right-0">
|
<div v-if="!props.sortable" class="absolute bottom-0 right-0">
|
||||||
<IconBtn>
|
<IconBtn @click.stop>
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
|
<VMenu v-model="menuVisible" activator="parent" close-on-content-click>
|
||||||
<VList>
|
<VList>
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ onMounted(() => {
|
|||||||
</VBtn>
|
</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" />
|
<VIcon icon="mdi-dots-vertical" size="20" />
|
||||||
<VMenu :activator="'parent'" :close-on-content-click="true" :location="'left'">
|
<VMenu :activator="'parent'" :close-on-content-click="true" :location="'left'">
|
||||||
<VList>
|
<VList>
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ function handleCardClick() {
|
|||||||
:ripple="!props.batchMode && !props.sortable"
|
:ripple="!props.batchMode && !props.sortable"
|
||||||
>
|
>
|
||||||
<div v-if="!props.sortable" class="me-n3 absolute top-1 right-4">
|
<div v-if="!props.sortable" class="me-n3 absolute top-1 right-4">
|
||||||
<IconBtn>
|
<IconBtn @click.stop>
|
||||||
<VIcon icon="mdi-dots-vertical" color="white" />
|
<VIcon icon="mdi-dots-vertical" color="white" />
|
||||||
<VMenu activator="parent" close-on-content-click>
|
<VMenu activator="parent" close-on-content-click>
|
||||||
<VList>
|
<VList>
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ interface VirtualCell {
|
|||||||
key: ItemKey
|
key: ItemKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface VirtualRange {
|
||||||
|
endIndex: number
|
||||||
|
endRow: number
|
||||||
|
startIndex: number
|
||||||
|
startRow: number
|
||||||
|
}
|
||||||
|
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
const trackRef = ref<HTMLElement | null>(null)
|
const trackRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
@@ -45,6 +52,8 @@ const layoutWidth = ref(0)
|
|||||||
const viewportTop = ref(0)
|
const viewportTop = ref(0)
|
||||||
const viewportBottom = ref(0)
|
const viewportBottom = ref(0)
|
||||||
const heightVersion = ref(0)
|
const heightVersion = ref(0)
|
||||||
|
const frozenVisibleRange = ref<VirtualRange | null>(null)
|
||||||
|
const isOverlayGrid = ref(false)
|
||||||
|
|
||||||
const itemHeights = new Map<ItemKey, number>()
|
const itemHeights = new Map<ItemKey, number>()
|
||||||
const observedElements = new Map<HTMLElement, ItemKey>()
|
const observedElements = new Map<HTMLElement, ItemKey>()
|
||||||
@@ -53,6 +62,7 @@ const itemRefCallbacks = new Map<ItemKey, (element: Element | ComponentPublicIns
|
|||||||
|
|
||||||
let resizeObserver: ResizeObserver | null = null
|
let resizeObserver: ResizeObserver | null = null
|
||||||
let itemResizeObserver: ResizeObserver | null = null
|
let itemResizeObserver: ResizeObserver | null = null
|
||||||
|
let overlayLockObserver: MutationObserver | null = null
|
||||||
let scrollTarget: ScrollTarget | null = null
|
let scrollTarget: ScrollTarget | null = null
|
||||||
let layoutFrameId: number | null = null
|
let layoutFrameId: number | null = null
|
||||||
let scrollFrameId: number | null = null
|
let scrollFrameId: number | null = null
|
||||||
@@ -149,7 +159,18 @@ const rowMetrics = computed(() => {
|
|||||||
|
|
||||||
const totalHeight = computed(() => rowMetrics.value.totalHeight)
|
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
|
const { heights, offsets, rowCount } = rowMetrics.value
|
||||||
|
|
||||||
if (!props.items.length || rowCount === 0) {
|
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 visibleCells = computed<VirtualCell[]>(() => {
|
||||||
const cells: VirtualCell[] = []
|
const cells: VirtualCell[] = []
|
||||||
|
|
||||||
@@ -190,7 +213,13 @@ const visibleCells = computed<VirtualCell[]>(() => {
|
|||||||
return cells
|
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(() => {
|
const visibleBlockHeight = computed(() => {
|
||||||
if (!props.items.length || visibleRange.value.endIndex <= visibleRange.value.startIndex) {
|
if (!props.items.length || visibleRange.value.endIndex <= visibleRange.value.startIndex) {
|
||||||
@@ -206,6 +235,10 @@ const visibleBlockHeight = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const bottomSpacerHeight = computed(() => {
|
const bottomSpacerHeight = computed(() => {
|
||||||
|
if (isOverlayGrid.value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
return Math.max(totalHeight.value - topSpacerHeight.value - visibleBlockHeight.value, 0)
|
return Math.max(totalHeight.value - topSpacerHeight.value - visibleBlockHeight.value, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -266,6 +299,45 @@ function findLastRowAtOrBeforeOffset(offsets: number[], rowCount: number, offset
|
|||||||
return answer
|
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 {
|
function getElementFromRef(element: Element | ComponentPublicInstance | null): HTMLElement | null {
|
||||||
if (!element || typeof HTMLElement === 'undefined') {
|
if (!element || typeof HTMLElement === 'undefined') {
|
||||||
return null
|
return null
|
||||||
@@ -312,6 +384,11 @@ function ensureItemResizeObserver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemResizeObserver = new ResizeObserver(entries => {
|
itemResizeObserver = new ResizeObserver(entries => {
|
||||||
|
if (shouldPauseVirtualSync()) {
|
||||||
|
freezeVisibleRange()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let shouldUpdate = false
|
let shouldUpdate = false
|
||||||
let scrollAdjustment = 0
|
let scrollAdjustment = 0
|
||||||
const currentViewportTop = viewportTop.value
|
const currentViewportTop = viewportTop.value
|
||||||
@@ -506,6 +583,15 @@ function queueLayoutSync() {
|
|||||||
|
|
||||||
layoutFrameId = window.requestAnimationFrame(() => {
|
layoutFrameId = window.requestAnimationFrame(() => {
|
||||||
layoutFrameId = null
|
layoutFrameId = null
|
||||||
|
|
||||||
|
if (shouldPauseVirtualSync()) {
|
||||||
|
freezeVisibleRange()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗内容已经由 overlay 限定生命周期,直接完整渲染可避免弹窗内交互被虚拟回收打断。
|
||||||
|
syncOverlayGridState()
|
||||||
|
releaseVisibleRange()
|
||||||
syncLayoutWidth()
|
syncLayoutWidth()
|
||||||
refreshScrollTarget()
|
refreshScrollTarget()
|
||||||
syncViewport()
|
syncViewport()
|
||||||
@@ -520,6 +606,13 @@ function queueViewportSync() {
|
|||||||
|
|
||||||
scrollFrameId = window.requestAnimationFrame(() => {
|
scrollFrameId = window.requestAnimationFrame(() => {
|
||||||
scrollFrameId = null
|
scrollFrameId = null
|
||||||
|
|
||||||
|
if (shouldPauseVirtualSync()) {
|
||||||
|
freezeVisibleRange()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseVisibleRange()
|
||||||
syncViewport()
|
syncViewport()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -681,6 +774,7 @@ function invalidateMeasurementsForLayoutChange() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
mounted = true
|
mounted = true
|
||||||
|
syncOverlayGridState()
|
||||||
scrollTarget = findScrollTarget()
|
scrollTarget = findScrollTarget()
|
||||||
addScrollListener(scrollTarget)
|
addScrollListener(scrollTarget)
|
||||||
|
|
||||||
@@ -689,6 +783,14 @@ onMounted(() => {
|
|||||||
resizeObserver.observe(trackRef.value)
|
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 })
|
window.addEventListener('resize', queueLayoutSync, { passive: true })
|
||||||
|
|
||||||
queueLayoutSync()
|
queueLayoutSync()
|
||||||
@@ -716,6 +818,8 @@ onUnmounted(() => {
|
|||||||
resizeObserver = null
|
resizeObserver = null
|
||||||
itemResizeObserver?.disconnect()
|
itemResizeObserver?.disconnect()
|
||||||
itemResizeObserver = null
|
itemResizeObserver = null
|
||||||
|
overlayLockObserver?.disconnect()
|
||||||
|
overlayLockObserver = null
|
||||||
|
|
||||||
if (layoutFrameId !== null) {
|
if (layoutFrameId !== null) {
|
||||||
window.cancelAnimationFrame(layoutFrameId)
|
window.cancelAnimationFrame(layoutFrameId)
|
||||||
|
|||||||
Reference in New Issue
Block a user