该方案基于公司内部业务需求封装实现,高度计算相关参数可根据自身项目布局灵活调整。
背景
在公司现有的后台管理系统中,表格页面原本采用自然滚动的方式,用户需要滚动整个页面来浏览数据。随着业务发展,产品提出新需求:表格区域需要固定高度,仅表格内容区域滚动,同时分页器固定在表格底部。这一改动看似简单,实际涉及多个响应式计算的联动。
本文记录我封装的一个 Vue 3 Composable 函数 ------ useTableHeight,用于动态计算表格最大高度,使其恰好填满视口剩余空间,实现完美的表格自适应布局。
参考效果图(可根据实际情况去修改方法达到你所要的效果)

需求分析
要实现表格固定高度、局部滚动的自适应效果,需要解决以下核心痛点问题:
-
视口高度动态变化:用户调整浏览器窗口大小时,表格高度需实时自适应适配
-
分页器高度不固定:分页器会因页码数量过多自动换行,高度动态变动,影响表格可用空间
-
UI 状态联动影响:高级搜索栏、筛选面板的展开/收起,会改变页面布局,挤压表格空间
-
容器样式干扰 :表格父容器的
padding-bottom等样式属性,需要纳入高度计算逻辑 -
极端场景兼容:视口过小、页面布局异常时,需要设置最小高度保底,保证表格基础可读性
核心实现
整体实现采用纯计算逻辑与响应式副作用分离的思路,拆分通用计算函数和 Composable 业务逻辑,兼顾可复用性和可测试性。
1. 纯高度计算函数
抽取独立的高度计算纯函数,无副作用、可单独单元测试,统一高度计算规则。
javascript
export function calculateTableMaxHeight({
viewportHeight,
tableTop,
paginationHeight,
extraGap,
containerPaddingBottom = 0,
minHeight,
}) {
const availableHeight =
viewportHeight -
tableTop -
paginationHeight -
extraGap -
containerPaddingBottom
return Math.max(minHeight, Math.floor(availableHeight))
}
计算公式说明: 可用高度 = 浏览器视口高度 - 表格顶部距离视口顶部的偏移量 - 分页器高度 - 自定义额外间距 - 容器底部内边距 最终取值会和最小高度对比,取最大值,兜底极端布局场景。
2. Composable 核心封装
封装 useTableHeight 组合式函数,接收表格、分页器 DOM 实例及自定义配置,自动监听布局变化、动态更新表格高度。
javascript
export function useTableHeight(tableRef, paginationRef, options = {}) {
const tableMaxHeight = ref(0)
// 默认配置项
const minHeight = options.minHeight ?? 240
const extraGap = options.extraGap ?? 24
const containerRef = options.containerRef
const watchSources = Array.isArray(options.watchSources) ? options.watchSources : []
let resizeObserver = null
// 更新表格最大高度核心方法
const updateTableMaxHeight = async () => {
await nextTick()
const tableEl = tableRef?.value?.$el
const paginationEl = paginationRef?.value
if (!tableEl || !paginationEl) return
// 获取元素布局信息
const tableRect = tableEl.getBoundingClientRect()
const paginationHeight = paginationEl.getBoundingClientRect().height || 0
const containerEl = containerRef?.value
const containerPaddingBottom = containerEl
? Number.parseFloat(window.getComputedStyle(containerEl).paddingBottom) || 0
: 0
// 执行高度计算
tableMaxHeight.value = calculateTableMaxHeight({
viewportHeight: window.innerHeight,
tableTop: tableRect.top,
paginationHeight,
extraGap,
containerPaddingBottom,
minHeight,
})
}
// 初始化元素尺寸监听
const initTableHeightObserver = () => {
updateTableMaxHeight()
// 创建 ResizeObserver 监听表格、分页器、容器尺寸变化
resizeObserver = new ResizeObserver(() => {
updateTableMaxHeight()
})
if (tableRef.value?.$el) resizeObserver.observe(tableRef.value.$el)
if (paginationRef.value) resizeObserver.observe(paginationRef.value)
if (containerRef?.value) resizeObserver.observe(containerRef.value)
}
// 挂载生命周期
onMounted(() => {
initTableHeightObserver()
window.addEventListener('resize', updateTableMaxHeight)
})
// 卸载生命周期,清理监听
onUnmounted(() => {
window.removeEventListener('resize', updateTableMaxHeight)
resizeObserver?.disconnect()
resizeObserver = null
})
// 监听外部响应式状态变化(如搜索栏展开收起)
watch(watchSources, async () => {
await nextTick()
updateTableMaxHeight()
})
return { tableMaxHeight, updateTableMaxHeight, initTableHeightObserver }
}
3. 关键技术点解析
nextTick 异步更新
高度计算前强制执行 await nextTick(),确保 Vue 完成本轮 DOM 更新渲染。尤其在监听外部状态(高级搜索展开/收起)变化时,布局会发生变动,必须等待 DOM 更新后再读取元素位置、尺寸信息,避免取值偏差。
ResizeObserver + window.resize 双监听
-
window.resize:监听浏览器视口整体尺寸变化,适配窗口缩放场景 -
ResizeObserver:精准监听局部 DOM 变化,包括分页器换行高度变化、容器尺寸变动、表格自身布局变化
两者结合可覆盖所有布局变动场景,解决单一监听的局限性。
DOM 信息获取方案
-
getBoundingClientRect:获取元素相对于视口的精准位置、宽高尺寸 -
getComputedStyle:读取元素实际渲染的 CSS 样式,精准获取容器padding-bottom等布局属性
实际调用示例
以下为后台带宽监控列表页面的真实落地用法,搭配 Element Plus 表格组件使用。
javascript
<template>
<div class="app-container-all">
<AdvancedSearch :show="showAdvancedSearch">
<!-- 搜索条件 -->
</AdvancedSearch>
<div class="top-container-control">
<!-- 操作按钮 -->
</div>
<div ref="tableWrapperRef" class="table-wrapper" v-loading="loading">
<el-table ref="myTable" :data="list" :height="tableMaxHeight">
<!-- 表格列定义 -->
</el-table>
<div class="table-pagination" ref="paginationRef">
<Pagination v-model:page-size="queryParams.pageSize" ... />
</div>
</div>
</div>
</template>
<script setup>
import { useTableHeight } from '@/composables/useTableHeight'
import { useAdvancedSearch } from '@/composables/useAdvancedSearch'
// DOM 实例绑定
const myTable = ref(null)
const paginationRef = ref(null)
const tableWrapperRef = ref(null)
// 高级搜索状态监听
const searchFields = computed(() => [
queryParams.value.deviceName,
timeValue.value,
queryParams.value.wan,
])
const { showAdvancedSearch } = useAdvancedSearch(searchFields)
// 初始化表格高度自适应
const { tableMaxHeight } = useTableHeight(myTable, paginationRef, {
watchSources: [showAdvancedSearch], // 监听搜索栏展开收起
minHeight: 240,
extraGap: 24,
containerRef: tableWrapperRef, // 传入容器获取padding样式
})
</script>
配置项说明
| 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | minHeight | number | 240 | 表格最小高度(px),极端场景保底,保证基础可读性 | | extraGap | number | 24 | 页面额外间距补偿(px),适配页面留白、边框等占位 | | containerRef | Ref | undefined | 表格父容器DOM引用,用于读取容器 padding-bottom 样式 | | watchSources | array | \[\] | 需要监听的响应式数据列表,数据变化时自动重算表格高度 |
使用注意事项
1. 组件实例 DOM 获取
Element Plus 的 el-table 属于组件实例,真实 DOM 需要通过 tableRef.value?.$el 获取;若为原生 DOM 元素,直接使用 tableRef.value 即可。
2. 分页器引用要求
分页器组件必须暴露根 DOM 元素引用,保证 paginationRef.value 可直接调用 getBoundingClientRect() 获取尺寸信息。
3. 容器样式适配
表格父容器的 padding-bottom 会占用页面空间,必须通过 containerRef 传入参与计算;若无需精准适配,也可增大 extraGap 做兜底补偿。
4. 性能优化说明
ResizeObserver 虽高频触发,但内部仅做 DOM 读取和简单赋值操作,无复杂计算和 DOM 重绘逻辑,性能开销极低,业务场景下无需额外防抖处理。
总结与展望
本次封装的 useTableHeight 核心优势在于解耦、通用、稳健:
-
分离关注点:纯计算逻辑与响应式副作用拆分,代码可测试、可复用
-
精准监听 :结合
ResizeObserver实现局部布局精准监听,弥补原生 resize 缺陷 -
配置驱动:通过可配置参数适配不同页面布局,无硬编码,通用性极强
-
场景兜底:最小高度机制,完美兼容极端布局场景
目前该 Hook 已落地公司多个后台列表页面,稳定适配表格+分页器的经典业务场景。后续计划优化迭代,支持表格动态行高、展开行等复杂场景的自动高度适配,进一步提升通用性。