
Vue3 Element Plus表格高度撑满屏幕剩余空间适用场景后台页面中el-table占满「表格顶边 → 屏幕底边」之间的空间表格内部滚动页面不出现外层滚动条。技术栈Vue 3 TypeScript Element Plus先看运行效果页面最外侧没有纵向滚动条表格高度撑满屏幕剩余空间。一、核心思路不用 flex 猜父容器高度直接量表格高度 window.innerHeight - 锚点top - 分页高度 - 底部间距 - 页面根节点padding-bottom在el-table正上方放一个高度为 0 的锚点用getBoundingClientRect().top拿到表格起始位置上方不管有多少搜索框、标题都能自动适配。页面根节点observeStopClass指定的 class如果有padding会自动扣掉padding-bottom不用手算。┌──────────────── window.innerHeight ────────────────┐ │ 搜索区 / 标题 ... │ │ ─────── anchorTop ─────── │ │ ┌─────────────────────────────────────────────┐ │ │ │ el-table 滚动区算出来的高度 │ │ │ └─────────────────────────────────────────────┘ │ │ 分页器 │ │ 底部间距 bottomGap默认 16px │ │ 页面根 padding-bottom自动测量 │ └────────────────────────────────────────────────────┘二、第一步必做清除浏览器默认 margin这一步不做页面很容易出现多余的外层滚动条。浏览器默认给body留了约 8px 的 margin内容总高度会略大于视口整页就会多出一截滚动条。实际项目里几乎都会在入口文件里清掉。在src/App.vue里加上作用于全局style body, html, #app { margin: 0; } /style如果你的项目已有全局 reset如normalize.css、* { margin: 0 }等效果等价可跳过。没有的话务必写上。三、第二步新建 Composable新建文件src/composables/useTableFillHeight.ts完整复制以下代码import{ref,watch,nextTick,onActivated,typeRef,typeMaybeRefOrGetter,toValue,}fromvue;exportinterfaceUseTableFillHeightOptions{enabled?:MaybeRefOrGetterboolean;anchorRef:RefHTMLElement|null;containerRef?:RefHTMLElement|null;tableRef?:Ref{doLayout?:()void}|null|undefined;bottomSelectors?:string[];bottomGap?:number;minHeight?:number;observeStopClass?:string;}constDEFAULT_BOTTOM_SELECTORS[.pagination-container];functionmeasureBottomReservedHeight(container:HTMLElement|null,selectors:string[],){if(!container)return0;letheight0;for(constselectorofselectors){constelcontainer.querySelector(selector)asHTMLElement|null;if(el)heightel.offsetHeight;}returnheight;}exportfunctionuseTableFillHeight(options:UseTableFillHeightOptions){const{anchorRef,containerRef,tableRef,bottomSelectorsDEFAULT_BOTTOM_SELECTORS,bottomGap16,minHeight300,observeStopClass,enabledtrue,}options;consttableHeightref(600);letobserver:ResizeObserver|nullnull;functionisEnabled(){returntoValue(enabled)!false;}functionfindPageRoot(anchorEl:HTMLElement,stopClass?:string){letel:HTMLElement|nullanchorEl;while(el){if(stopClassel.classList.contains(stopClass))returnel;elel.parentElement;}returnnull;}functionmeasurePageRootPaddingBottom(anchorEl:HTMLElement,stopClass?:string){constpageRootfindPageRoot(anchorEl,stopClass);if(!pageRoot)return0;constpaddingBottomparseFloat(getComputedStyle(pageRoot).paddingBottom);returnNumber.isNaN(paddingBottom)?0:paddingBottom;}functionupdateTableHeight(){if(!isEnabled()||!anchorRef.value)return;constcontainercontainerRef?.value??null;constanchorTopanchorRef.value.getBoundingClientRect().top;constbottomReservedmeasureBottomReservedHeight(container,bottomSelectors,);constpageRootPaddingBottommeasurePageRootPaddingBottom(anchorRef.value,observeStopClass,);tableHeight.valueMath.max(minHeight,window.innerHeight-anchorTop-bottomReserved-bottomGap-pageRootPaddingBottom,);}functionscheduleUpdate(){nextTick((){updateTableHeight();requestAnimationFrame(updateTableHeight);});}functionsetup(){if(!isEnabled())return;scheduleUpdate();setTimeout(scheduleUpdate,150);window.addEventListener(resize,updateTableHeight);constrootElcontainerRef?.value??anchorRef.value;if(!rootEl)return;observernewResizeObserver(updateTableHeight);letel:HTMLElement|nullrootEl;letlevel0;while(ellevel5){observer.observe(el);if(observeStopClassel.classList.contains(observeStopClass))break;elel.parentElement;level;}}functioncleanup(){observer?.disconnect();window.removeEventListener(resize,updateTableHeight);}watch(tableHeight,(){nextTick(()tableRef?.value?.doLayout?.());});onActivated((){if(isEnabled())scheduleUpdate();});return{tableHeight,updateTableHeight:scheduleUpdate,setup,cleanup};}四、第三步页面示例完整复制运行新建src/views/Test01.vue或任意页面完整复制以下代码template div classtable-fill-demo !-- 模拟页面上方内容 -- div div模拟页面上方内容比如搜索框、按钮、功能区等/div div1/div div2/div div3/div div4/div div5/div /div !-- 表格区域 -- div refcontainerRef classtable-panel div classtable-header span classtable-title数据列表/span /div !-- 锚点紧贴 el-table 上方高度为 0 -- div reftableAnchorRef classtable-body-anchor / el-table reftableRef :datatableData :heighttableHeight border stylewidth: 100% el-table-column propid labelID width80 / el-table-column propname label名称 min-width120 / el-table-column propstatus label状态 width100 / el-table-column propdate label日期 min-width160 / /el-table div classpagination-container el-pagination v-model:current-pagepageNo v-model:page-sizepageSize :totaltotal :page-sizes[10, 20, 50] layouttotal, sizes, prev, pager, next / /div /div /div /template script setup langts import { ref, onMounted, onBeforeUnmount } from vue; import { useTableFillHeight } from /composables/useTableFillHeight; const pageNo ref(1); const pageSize ref(10); const total ref(100); const containerRef refHTMLElement | null(null); const tableAnchorRef refHTMLElement | null(null); const tableRef ref(); const { tableHeight, setup, cleanup } useTableFillHeight({ anchorRef: tableAnchorRef, containerRef, tableRef, observeStopClass: table-fill-demo, }); // 模拟数据 const tableData ref( Array.from({ length: 50 }, (_, i) ({ id: i 1, name: 示例数据 ${i 1}, status: i % 2 0 ? 正常 : 异常, date: 2026-07-09 10:00:00, })), ); onMounted(() { setup(); }); onBeforeUnmount(() { cleanup(); }); /script style scoped langscss .table-fill-demo { padding: 20px; box-sizing: border-box; background: #fff; } .table-panel { .table-header { margin-bottom: 8px; .table-title { font-size: 14px; font-weight: bold; color: #303133; } } /* 锚点高度为 0不占视觉空间 */ .table-body-anchor { height: 0; } .pagination-container { padding: 10px 0; display: flex; justify-content: flex-end; } } /style五、第四步配置路由在router/index.ts里加一条路由路径按项目实际情况调整{path:/test01,name:Test01,component:()import(/views/Test01.vue),}浏览器访问该路由表格会自动撑满剩余屏幕高度。六、接入你自己的页面只需记住 4 件事1. 全局清掉 body 默认 margin见第二节2. 模板加锚点 分页容器div reftableAnchorRef classtable-body-anchor / el-table reftableRef :heighttableHeight ... / div classpagination-container el-pagination ... / /div分页容器 class 默认是.pagination-container如果类名不同通过bottomSelectors传入。3. 脚本核心调用const{tableHeight,setup,cleanup}useTableFillHeight({anchorRef:tableAnchorRef,containerRef,tableRef,observeStopClass:你的页面根元素class,// 与页面最外层 div 的 class 一致});onMounted(setup);onBeforeUnmount(cleanup);observeStopClass有两个作用ResizeObserver向上监听时遇到该 class 就停止避免监听到无关父级自动读取该元素的padding-bottom参与高度计算4. 样式页面根节点.your-page-root { padding: 20px; // padding-bottom 会自动扣掉不用手算 box-sizing: border-box; // 必写让 padding 不撑大布局 background: #fff; } .table-body-anchor { height: 0; // 锚点不占视觉空间 }七、高度计算公式constanchorTopanchorRef.value.getBoundingClientRect().top;constbottomReserved分页器高度;// 自动测量 .pagination-containerconstpageRootPaddingBottom页面根节点 padding-bottom;// 由 observeStopClass 自动测量tableHeightwindow.innerHeight-anchorTop-bottomReserved-bottomGap-pageRootPaddingBottom;举例窗口高 900px锚点距顶部 200px分页 52pxbottomGap16px页面根padding-bottom20px900 - 200 - 52 - 16 - 20 612px八、配置项说明参数类型默认值说明anchorRefRefHTMLElement必填锚点元素紧贴el-table上方containerRefRefHTMLElement可选表格容器用于测量分页高度tableRefRef可选el-table实例高度变化后自动doLayoutobserveStopClassstring可选页面根 class控制监听边界 自动扣 padding-bottombottomSelectorsstring[][.pagination-container]底部需预留高度的元素选择器bottomGapnumber16表格底到视口底的额外间距minHeightnumber300表格最小高度enabledbooleantrue是否启用计算九、常见问题现象原因处理页面出现外层滚动条App.vue没清body默认 margin见第二节给body, html, #app设margin: 0表格高度一直是 600没调setup()或锚点 ref 未绑定检查onMounted(setup)底部留白太多/太少bottomGap不匹配调整bottomGap默认 16底部还多出一截空白页面根有padding但没传observeStopClass传入与页面根一致的observeStopClass没有分页器底部分页高度为 0正常不影响计算搜索区展开后高度不对布局变化后需重算调updateTableHeight()keep-alive 切回后高度不对页面激活时未重算composable 已内置onActivated确认调了setup()搜索区展开/收起时手动刷新const{tableHeight,updateTableHeight,setup,cleanup}useTableFillHeight({...});functiononSearchToggle(){updateTableHeight();}十、为什么不用 flex方式问题flex: 1撑满Element Plus Tabs 等中间层clientHeight常为 0写死height600大屏浪费、小屏溢出监听自身容器算高度表格变高 → 容器变高 → 越算越高 → 页面滚动条视口定位法只量锚点到屏幕底部的距离不依赖 flex 链路也不会自我膨胀。十一、总结App.vue清掉body默认 margin— 不做就容易出现整页滚动条锚点放el-table正上方高度为 0useTableFillHeight算出tableHeight绑到:heightobserveStopClass传页面根 class自动处理padding-bottom和监听边界页面根节点加box-sizing: border-box滚动发生在表格内部复制第二节 第三节 第四节代码即可直接预览效果。如果本文对你有帮助欢迎点赞收藏。