import { nextTick, onBeforeUnmount, onMounted, ref, unref, watch, type Ref } from 'vue' import { useEventListener, useResizeObserver } from '@vueuse/core' import { LIST_TABLE_VIEWPORT_BOTTOM_GAP, LIST_TABLE_VIEWPORT_MIN_HEIGHT, calcListTableViewportHeight } from '@/composables/listTableViewportHeight' export type UseListTableViewportHeightOptions = { /** 表格锚点(包住 ListTable/el-table 的元素,不含分页) */ tableEl: Ref | (() => HTMLElement | null | undefined) /** 分页条根节点;无分页可省略 */ pagerEl?: Ref | (() => HTMLElement | null | undefined) /** 可选:ListTable / el-table 组件实例,高度变化后调用 doLayout */ tableRef?: Ref minHeight?: number bottomGap?: number /** 主内容滚动根;默认 #app-content */ scrollRootSelector?: string /** 为 false 时不自动监听(隐藏 Tab 可用) */ enabled?: Ref | boolean } function resolveEl( source?: Ref | (() => HTMLElement | null | undefined) ): HTMLElement | null { if (!source) return null if (typeof source === 'function') return source() ?? null return unref(source) ?? null } export function useListTableViewportHeight(options: UseListTableViewportHeightOptions) { const tableHeight = ref(LIST_TABLE_VIEWPORT_MIN_HEIGHT) const minHeight = options.minHeight ?? LIST_TABLE_VIEWPORT_MIN_HEIGHT const bottomGap = options.bottomGap ?? LIST_TABLE_VIEWPORT_BOTTOM_GAP const scrollRootSelector = options.scrollRootSelector ?? '#app-content' const isEnabled = () => { const e = options.enabled if (e === undefined) return true return !!unref(e) } const recalc = () => { if (!isEnabled()) return if (typeof window === 'undefined') return const table = resolveEl(options.tableEl) if (!table) return const root = (document.querySelector(scrollRootSelector) as HTMLElement | null) ?? null const contentViewportBottom = root ? root.getBoundingClientRect().bottom : window.innerHeight const tableTop = table.getBoundingClientRect().top const pager = resolveEl(options.pagerEl) const pagerHeight = pager ? Math.ceil(pager.getBoundingClientRect().height) : 0 const next = calcListTableViewportHeight({ contentViewportBottom, tableTop, pagerHeight, minHeight, bottomGap }) if (next !== tableHeight.value) { tableHeight.value = next } nextTick(() => { const inst = options.tableRef ? unref(options.tableRef) : null if (inst && typeof inst.doLayout === 'function') { inst.doLayout() } }) } let raf = 0 const scheduleRecalc = () => { if (raf) cancelAnimationFrame(raf) raf = requestAnimationFrame(() => { raf = 0 recalc() }) } onMounted(() => { nextTick(scheduleRecalc) }) onBeforeUnmount(() => { if (raf) cancelAnimationFrame(raf) }) useEventListener(window, 'resize', scheduleRecalc) // 观察表锚点与分页尺寸变化(筛选展开会改变 tableTop) useResizeObserver( () => resolveEl(options.tableEl), () => scheduleRecalc() ) useResizeObserver( () => resolveEl(options.pagerEl), () => scheduleRecalc() ) useResizeObserver( () => document.querySelector(scrollRootSelector) as HTMLElement | null, () => scheduleRecalc() ) if (options.enabled !== undefined && typeof options.enabled !== 'boolean') { watch(options.enabled, () => scheduleRecalc()) } return { tableHeight, recalc: scheduleRecalc } }