Measure content viewport, table top, and pager to drive el-table height. Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
3.6 KiB
TypeScript
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 }
|
|
}
|