diff --git a/web-admin/src/composables/useListTableViewportHeight.ts b/web-admin/src/composables/useListTableViewportHeight.ts new file mode 100644 index 000000000..07ebe2a52 --- /dev/null +++ b/web-admin/src/composables/useListTableViewportHeight.ts @@ -0,0 +1,127 @@ +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 } +}