Files
shanxia2.0/web-admin/src/composables/useListTableViewportHeight.ts
T
zlwuandCursor 55ac1ebbf6 feat(web-admin): add useListTableViewportHeight composable
Measure content viewport, table top, and pager to drive el-table height.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 18:45:22 +08:00

128 lines
3.6 KiB
TypeScript

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> | (() => HTMLElement | null | undefined)
/** 分页条根节点;无分页可省略 */
pagerEl?: Ref<HTMLElement | null | undefined> | (() => HTMLElement | null | undefined)
/** 可选:ListTable / el-table 组件实例,高度变化后调用 doLayout */
tableRef?: Ref<any>
minHeight?: number
bottomGap?: number
/** 主内容滚动根;默认 #app-content */
scrollRootSelector?: string
/** 为 false 时不自动监听(隐藏 Tab 可用) */
enabled?: Ref<boolean> | boolean
}
function resolveEl(
source?: Ref<HTMLElement | null | undefined> | (() => 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 }
}