mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
refactor: extract available height calculation logic into a reusable useAvailableHeight composable
This commit is contained in:
@@ -13,6 +13,7 @@ import MediaInfoDialog from '../dialog/MediaInfoDialog.vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
import { useBackgroundOptimization } from '@/composables/useBackgroundOptimization'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
|
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -23,6 +24,10 @@ const display = useDisplay()
|
|||||||
|
|
||||||
const { appMode } = usePWA()
|
const { appMode } = usePWA()
|
||||||
|
|
||||||
|
// 计算列表可用高度
|
||||||
|
// componentOffset = FileToolbar(48) + FileList操作栏(40) + VCard边距(4) = 92
|
||||||
|
const { availableHeight: listAvailableHeight } = useAvailableHeight(92, 300)
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const inProps = defineProps({
|
const inProps = defineProps({
|
||||||
icons: Object,
|
icons: Object,
|
||||||
@@ -143,29 +148,7 @@ const transferItems = ref<FileItem[]>([])
|
|||||||
// 当前图片地址
|
// 当前图片地址
|
||||||
const currentImgLink = ref('')
|
const currentImgLink = ref('')
|
||||||
|
|
||||||
// 计算列表可用高度
|
|
||||||
const listAvailableHeight = computed(() => {
|
|
||||||
// 获取视口高度
|
|
||||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight
|
|
||||||
|
|
||||||
// navbar高度
|
|
||||||
const navbarHeight = 72
|
|
||||||
// 工具栏高度(包含搜索框和按钮)
|
|
||||||
const toolbarHeight = 64
|
|
||||||
// 底部导航栏高度
|
|
||||||
const footerHeight = appMode.value ? 80 : 16
|
|
||||||
// 安全区域高度
|
|
||||||
const safeAreaHeight =
|
|
||||||
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-area-inset-bottom')) ||
|
|
||||||
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-area-inset-top')) ||
|
|
||||||
0
|
|
||||||
|
|
||||||
// 计算可用高度,预留一些边距
|
|
||||||
const availableHeight = viewportHeight - navbarHeight - toolbarHeight - footerHeight - safeAreaHeight - 40
|
|
||||||
|
|
||||||
// 确保最小高度
|
|
||||||
return Math.max(availableHeight, 300)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 是否为图片文件
|
// 是否为图片文件
|
||||||
const isImage = computed(() => {
|
const isImage = computed(() => {
|
||||||
|
|||||||
@@ -5,38 +5,18 @@ import { useDisplay } from 'vuetify'
|
|||||||
import type { AxiosRequestConfig, AxiosInstance } from 'axios'
|
import type { AxiosRequestConfig, AxiosInstance } from 'axios'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
|
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 显示器宽度
|
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
|
|
||||||
const { appMode } = usePWA()
|
const { appMode } = usePWA()
|
||||||
|
|
||||||
// 计算列表可用高度
|
// 计算列表可用高度
|
||||||
const availableHeight = computed(() => {
|
// componentOffset = FileToolbar(48) = 48
|
||||||
// 获取视口高度
|
const { availableHeight } = useAvailableHeight(48, 300)
|
||||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight
|
|
||||||
|
|
||||||
// navbar高度
|
|
||||||
const navbarHeight = 72
|
|
||||||
// 工具栏高度
|
|
||||||
const toolbarHeight = 25
|
|
||||||
// 底部导航栏高度
|
|
||||||
const footerHeight = appMode.value ? 80 : 16
|
|
||||||
// 安全区域高度
|
|
||||||
const safeAreaHeight =
|
|
||||||
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-area-inset-bottom')) ||
|
|
||||||
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-area-inset-top')) ||
|
|
||||||
0
|
|
||||||
|
|
||||||
// 计算可用高度,预留一些边距
|
|
||||||
const availableHeight = viewportHeight - navbarHeight - toolbarHeight - footerHeight - safeAreaHeight - 40
|
|
||||||
|
|
||||||
// 确保最小高度
|
|
||||||
return Math.max(availableHeight, 300)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { usePWA } from '@/composables/usePWA'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算页面内容的可用高度,自动适配 iOS 安全区域和底部 Dock 栏。
|
||||||
|
*
|
||||||
|
* 在 appMode(PWA 小屏)下,底部 Dock(Footer)通过 Teleport 挂载到 body,
|
||||||
|
* 始终可见并悬浮在底部。本 composable 会测量 Dock 的实际 DOM 高度(已包含
|
||||||
|
* safe-area-inset-bottom),从而自适应不同 iOS 设备的安全区域。
|
||||||
|
*
|
||||||
|
* 计算公式: viewport - navbarHeight - layoutPadding - footerDock - componentOffset
|
||||||
|
* 其中 componentOffset 是调用方指定的组件内部额外占用空间(如工具栏、分页栏等)
|
||||||
|
*
|
||||||
|
* @param componentOffset - 组件内部额外占用的空间(工具栏、分页栏等,默认 64)
|
||||||
|
* @param minHeight - 最小高度(默认 300)
|
||||||
|
*/
|
||||||
|
export function useAvailableHeight(
|
||||||
|
componentOffset: number = 64,
|
||||||
|
minHeight: number = 300,
|
||||||
|
) {
|
||||||
|
const { appMode } = usePWA()
|
||||||
|
|
||||||
|
// 响应式的视口高度,监听 resize 事件
|
||||||
|
const viewportHeight = ref(window.innerHeight || document.documentElement.clientHeight)
|
||||||
|
|
||||||
|
// Footer Dock 测量高度(响应式)
|
||||||
|
const footerDockMeasuredHeight = ref(0)
|
||||||
|
|
||||||
|
function updateMeasurements() {
|
||||||
|
viewportHeight.value = window.innerHeight || document.documentElement.clientHeight
|
||||||
|
|
||||||
|
// 测量 Footer Dock 的实际高度
|
||||||
|
// footer-nav-container 的 CSS 中已包含 env(safe-area-inset-bottom),
|
||||||
|
// 所以 offsetHeight 是包含安全区域的完整高度
|
||||||
|
if (appMode.value) {
|
||||||
|
const footerEl = document.querySelector('.footer-nav-container') as HTMLElement | null
|
||||||
|
footerDockMeasuredHeight.value = footerEl ? footerEl.offsetHeight : 70
|
||||||
|
} else {
|
||||||
|
footerDockMeasuredHeight.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始测量
|
||||||
|
updateMeasurements()
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateMeasurements)
|
||||||
|
// iOS 虚拟键盘弹出/收起时 visualViewport 会变化
|
||||||
|
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
|
||||||
|
|
||||||
|
// navbar 固定高度(对应 layout-page-content 的 padding-block-start: 4.5rem)
|
||||||
|
const navbarHeight = 72
|
||||||
|
|
||||||
|
// layout-page-content 的 padding-block-end (1.5rem = 24px)
|
||||||
|
const layoutBottomPadding = 24
|
||||||
|
|
||||||
|
// 底部 Dock 栏高度(appMode 下通过 DOM 测量,已含 safe-area-inset-bottom)
|
||||||
|
const footerDockHeight = footerDockMeasuredHeight.value
|
||||||
|
|
||||||
|
const available = vh - navbarHeight - layoutBottomPadding - footerDockHeight - componentOffset
|
||||||
|
|
||||||
|
return Math.max(available, minHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableHeight,
|
||||||
|
viewportHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { useDisplay } from 'vuetify'
|
|||||||
import { formatFileSize } from '@/@core/utils/formatters'
|
import { formatFileSize } from '@/@core/utils/formatters'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
|
import { useAvailableHeight } from '@/composables/useAvailableHeight'
|
||||||
|
|
||||||
// i18n
|
// i18n
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -21,6 +22,10 @@ const display = useDisplay()
|
|||||||
// PWA模式检测
|
// PWA模式检测
|
||||||
const { appMode } = usePWA()
|
const { appMode } = usePWA()
|
||||||
|
|
||||||
|
// 计算列表可用高度
|
||||||
|
// componentOffset = VCardItem搜索栏(80) + VDivider(1) + 分页栏(52) + VCard边距(4) = 137
|
||||||
|
const { availableHeight } = useAvailableHeight(137, 300)
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -242,29 +247,7 @@ const TransferDict: { [key: string]: string } = {
|
|||||||
rclone_move: t('transferHistory.transferMode.rclone_move'),
|
rclone_move: t('transferHistory.transferMode.rclone_move'),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算列表可用高度
|
|
||||||
const availableHeight = computed(() => {
|
|
||||||
// 获取视口高度
|
|
||||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight
|
|
||||||
|
|
||||||
// navbar高度
|
|
||||||
const navbarHeight = 72
|
|
||||||
// 工具栏高度
|
|
||||||
const toolbarHeight = 88
|
|
||||||
// 底部导航栏高度
|
|
||||||
const footerHeight = appMode.value ? 80 : 16
|
|
||||||
// 安全区域高度
|
|
||||||
const safeAreaHeight =
|
|
||||||
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-area-inset-bottom')) ||
|
|
||||||
parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--safe-area-inset-top')) ||
|
|
||||||
0
|
|
||||||
|
|
||||||
// 计算可用高度,预留一些边距
|
|
||||||
const availableHeight = viewportHeight - navbarHeight - toolbarHeight - footerHeight - safeAreaHeight - 48
|
|
||||||
|
|
||||||
// 确保最小高度
|
|
||||||
return Math.max(availableHeight, 300)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分页提示
|
// 分页提示
|
||||||
const pageTip = computed(() => {
|
const pageTip = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user