mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-11 18:10:49 +08:00
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||
import { usePWA } from '@/composables/usePWA'
|
||
|
||
/**
|
||
* 计算页面内容的可用高度,自动适配 iOS 安全区域和底部 Dock 栏。
|
||
*
|
||
* 通过 DOM 测量获取布局的实际 padding(含 safe-area-inset-top/bottom),
|
||
* 以及 Footer Dock 的实际高度,确保在任何设备上都不会被 Dock 遮挡。
|
||
*
|
||
* 计算公式: viewport - layoutPaddingTop - layoutPaddingBottom - footerDock - componentOffset
|
||
*
|
||
* @param componentOffset - 组件内部额外占用的空间(工具栏、分页栏等,默认 64)
|
||
* @param minHeight - 最小高度(默认 300)
|
||
*/
|
||
export function useAvailableHeight(
|
||
componentOffset: number = 64,
|
||
minHeight: number = 300,
|
||
) {
|
||
const { appMode } = usePWA()
|
||
|
||
// 响应式测量值
|
||
const viewportHeight = ref(window.innerHeight || document.documentElement.clientHeight)
|
||
const layoutPaddingTop = ref(72)
|
||
const layoutPaddingBottom = ref(24)
|
||
const footerDockMeasuredHeight = ref(0)
|
||
|
||
function updateMeasurements() {
|
||
viewportHeight.value = window.innerHeight || document.documentElement.clientHeight
|
||
|
||
// 测量 .layout-page-content 的实际 padding(含 env(safe-area-inset-top) 等)
|
||
const layoutEl = document.querySelector('.layout-page-content') as HTMLElement | null
|
||
if (layoutEl) {
|
||
const style = getComputedStyle(layoutEl)
|
||
layoutPaddingTop.value = parseFloat(style.paddingTop) || 72
|
||
layoutPaddingBottom.value = parseFloat(style.paddingBottom) || 24
|
||
}
|
||
|
||
// 直接查询 Footer Dock DOM,无论 appMode 状态
|
||
// Dock 通过 Teleport 挂载到 body,存在即测量,不存在即为 0
|
||
const footerEl = document.querySelector('.footer-nav-container') as HTMLElement | null
|
||
footerDockMeasuredHeight.value = footerEl ? footerEl.offsetHeight : 0
|
||
}
|
||
|
||
// appMode 异步变化时(PWA 检测完成、屏幕尺寸变化等),Dock 会出现/消失
|
||
// 需要等 DOM 更新后重新测量
|
||
watch(appMode, () => {
|
||
nextTick(updateMeasurements)
|
||
})
|
||
|
||
onMounted(() => {
|
||
nextTick(updateMeasurements)
|
||
|
||
window.addEventListener('resize', updateMeasurements)
|
||
if (window.visualViewport) {
|
||
window.visualViewport.addEventListener('resize', updateMeasurements)
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', updateMeasurements)
|
||
if (window.visualViewport) {
|
||
window.visualViewport.removeEventListener('resize', updateMeasurements)
|
||
}
|
||
})
|
||
|
||
const availableHeight = computed(() => {
|
||
const vh = viewportHeight.value
|
||
|
||
// 布局顶部 padding(含 safe-area-inset-top + navbar 高度)
|
||
const topPadding = layoutPaddingTop.value
|
||
|
||
// 布局底部 padding
|
||
const bottomPadding = layoutPaddingBottom.value
|
||
|
||
// 底部 Dock 栏遮挡高度(通过 DOM 测量,含 safe-area-inset-bottom)
|
||
const footerDockHeight = footerDockMeasuredHeight.value
|
||
|
||
const available = vh - topPadding - bottomPadding - footerDockHeight - componentOffset
|
||
|
||
return Math.max(available, minHeight)
|
||
})
|
||
|
||
return {
|
||
availableHeight,
|
||
viewportHeight,
|
||
}
|
||
}
|