后台系统里面表格页面很多,列配置一般还是固定写在页面里,这里主要记录一下 Element Plus 表格高度自适应的处理
后台项目中,列表页是最常见的页面。
我的表格列并不是动态配置出来的,还是正常在页面里写 el-table-column,这样看起来更直观,后期改某一列也方便。
真正需要统一处理的是表格高度。
因为后台页面经常会有搜索区域、分页、顶部标签页、左侧菜单、浏览器窗口变化等情况,如果表格高度不处理好,就很容易出现下面这些问题:
- 页面外层多出一个滚动条
- 表格内部也有滚动条,变成双滚动
- fixed 固定列错位
- 搜索条件换行之后,表格把分页挤出页面
- 切换标签页回来之后,高度需要刷新一下才正常
- 弹窗里面放表格时,弹窗和表格同时滚动
所以这篇主要记录后台项目里表格高度怎么处理得稳定一点。
页面结构
页面结构大概是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <div class="page-layout table-demo-page"> <div class="search-component"> 搜索区域 </div>
<div ref="table" class="Table-page"> <el-table ref="tableList" v-loading="tableLoading" :data="tableData" stripe border :max-height="tableHeight" size="large" > <el-table-column align="center" prop="id" label="编号" width="90" /> <el-table-column align="center" prop="name" label="名称" min-width="160" /> <el-table-column align="center" prop="createdAt" label="创建时间" :formatter="formatterTime" min-width="180" /> </el-table>
<Pagination v-model="page" :total="total" @handleCurrentChange="changePage" /> </div> </div>
|
这里主要注意两个地方:
ref="table":外层表格区域,用来计算位置和分页高度
:max-height="tableHeight":Element Plus 表格最大高度
列还是按普通写法写,重点是把 tableHeight 算准确。
如果老项目里面字段已经叫 tableHight 这种拼错的名字,也没必要为了一个变量名到处改,保持当前项目能稳定运行更重要。
为什么用max-height
Element Plus 的表格如果不限制高度,数据多的时候会直接把页面撑开。
后台页面一般不希望整页一直往下滚,而是希望:
- 搜索区域固定在上面
- 分页固定在表格下面
- 表格内容区域内部滚动
- 外层布局不要出现多余滚动条
所以这里使用 max-height。
1 2
| <el-table :max-height="tableHeight"> </el-table>
|
当数据多的时候,表格内部会出现滚动条,页面整体高度就比较稳定。
外层布局要先稳住
表格高度不是只靠表格自己能解决的。
如果外层布局本身没有形成完整的高度链路,表格再怎么算也容易出现滚动条。
后台布局里一般要保证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| .common-layout, .el-container, .main-layout { height: 100%; min-height: 0; overflow: hidden; }
.view-scroll { flex: 1; min-height: 0; height: 100%; }
.page-layout { display: flex; flex-direction: column; height: 100%; min-height: 100%; overflow: hidden; }
.search-component { flex-shrink: 0; }
.Table-page { display: flex; flex: 1; flex-direction: column; min-height: 0; overflow: hidden; }
|
这里最容易忽略的是 min-height: 0,它能避免 flex 子元素把父级撑开。
初始字段
我把表格高度相关的字段放在 tableMixin.js 中:
1 2 3 4 5 6 7 8 9
| data() { return { tableHeight: 300, marginHeight: 80, tableResizeObserver: null, tableHeightFrame: null, tableLayoutFrame: null }; }
|
其中:
tableHeight:最终给 el-table 使用的高度
marginHeight:没有分页时预留的底部高度
tableResizeObserver:监听布局尺寸变化
tableHeightFrame:控制高度计算的节流
tableLayoutFrame:控制表格重新布局
获取表格容器
由于有些页面 ref 可能拿到组件实例,有些可能拿到原生 DOM,所以先封装一个方法:
1 2 3 4
| getTableWrapper() { const tableRef = this.$refs.table; return tableRef?.$el || tableRef || null; }
|
这样后面统一拿到真实 DOM。
另外我再封装一个当前元素:
1 2 3 4 5 6 7 8 9 10 11 12
| getCurrentElement() { if (this.$el && this.$el.nodeType === 1) { return this.$el; }
const tableDom = this.getTableWrapper(); if (tableDom && tableDom.nodeType === 1) { return tableDom; }
return null; }
|
这样在页面初次渲染、切换标签、弹窗打开这些场景里更稳一点。
计算高度
核心方法是 viewHeight。
大概思路是:
- 获取外层滚动容器高度
- 获取表格区域距离容器顶部的距离
- 获取分页高度
- 减去页面底部 padding
- 得到表格可用高度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| viewHeight() { try { const tableDom = this.getTableWrapper();
if (!tableDom) { return; }
const currentDom = this.getCurrentElement(); const viewScrollDom = currentDom?.closest('.view-scroll'); const pageLayoutDom = currentDom?.closest('.page-layout') || currentDom; const paginationDom = tableDom.querySelector('.el-footer'); const viewScrollRect = viewScrollDom?.getBoundingClientRect(); const tableRect = tableDom.getBoundingClientRect(); const paginationHeight = paginationDom?.offsetHeight || 0; const pagePaddingBottom = pageLayoutDom ? parseFloat(window.getComputedStyle(pageLayoutDom).paddingBottom) || 0 : 0; const viewHeight = viewScrollDom?.clientHeight || pageLayoutDom?.clientHeight || window.innerHeight; const tableOffsetTop = viewScrollRect ? Math.max(tableRect.top - viewScrollRect.top, 0) : 0;
let nextTableHeight = viewHeight - tableOffsetTop - paginationHeight - pagePaddingBottom;
if (!paginationDom) { nextTableHeight -= this.marginHeight; }
nextTableHeight = Math.max(Math.floor(nextTableHeight), 160);
if (Math.abs(this.tableHeight - nextTableHeight) > 1) { this.tableHeight = nextTableHeight; } } catch (e) { console.log(e); } }
|
这里没有直接用 window.innerHeight 去减一个写死的头部高度,因为真实后台里头部、标签栏、搜索区、按钮区、分页区都可能变化。
为什么要减分页高度
表格区域下面一般会有分页组件。
如果不减分页高度,表格就会把分页挤到下面,导致外层页面出现滚动条。
所以这里通过:
1 2
| const paginationDom = tableDom.querySelector('.el-footer'); const paginationHeight = paginationDom?.offsetHeight || 0;
|
拿到分页高度,再从总高度中减掉。
项目里面如果分页组件外层不是 .el-footer,就换成自己分页组件的 class。
为什么要减tableOffsetTop
搜索区域的高度不是固定的。
有的页面搜索条件少,有的页面搜索条件多,甚至还有展开收起。
所以不能写死一个高度。
这里用表格区域距离外层滚动容器顶部的距离来计算:
1
| const tableOffsetTop = viewScrollRect ? Math.max(tableRect.top - viewScrollRect.top, 0) : 0;
|
这样搜索区域变高或者变低,表格高度都能跟着变化。
弹窗里的表格
还有一种情况比较容易出问题:弹窗里面放表格。
如果继续按页面列表的逻辑算高度,弹窗经常会变成:
- 弹窗外层滚动
- 表格内部滚动
- 页面底部也跟着滚
这种体验很别扭。
所以我会在 viewHeight 里面判断当前表格是不是在弹窗里面:
1 2 3 4 5 6 7 8 9 10 11
| const dialogBodyDom = tableDom.closest('.el-dialog__body');
if (dialogBodyDom) { const nextTableHeight = this.getDialogTableHeight(tableDom);
if (Math.abs(this.tableHeight - nextTableHeight) > 1) { this.tableHeight = nextTableHeight; }
return; }
|
弹窗表格单独算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| getDialogTableHeight(tableDom, minHeight = 160) { const tableElement = tableDom?.$el || tableDom;
if (!tableElement) { return minHeight; }
const dialogDom = tableElement.closest('.el-dialog'); const dialogFooterDom = dialogDom?.querySelector('.el-dialog__footer'); const paginationDom = tableElement.querySelector('.el-footer'); const dialogStyle = dialogDom ? window.getComputedStyle(dialogDom) : null; const tableRect = tableElement.getBoundingClientRect(); const paginationHeight = paginationDom?.offsetHeight || 0; const footerHeight = dialogFooterDom?.offsetHeight || 0; const dialogBottomSpace = parseFloat(dialogStyle?.marginBottom) || window.innerHeight * 0.05;
let nextTableHeight = window.innerHeight - tableRect.top - paginationHeight - footerHeight - dialogBottomSpace - 16;
if (!paginationDom) { nextTableHeight -= this.marginHeight; }
return Math.max(Math.floor(nextTableHeight), minHeight); }
|
这样弹窗里也能让表格内部滚动。
requestAnimationFrame
高度计算会因为窗口变化、页面更新、搜索区域变化触发很多次。
如果每次都直接计算,可能会频繁触发页面回流。
所以我这里加了一层:
1 2 3 4 5 6 7 8 9
| scheduleViewHeight() { if (this.tableHeightFrame) { cancelAnimationFrame(this.tableHeightFrame); }
this.tableHeightFrame = requestAnimationFrame(() => { this.viewHeight(); }); }
|
这样同一帧里面多次触发,也只会执行最后一次。
ResizeObserver
只监听窗口 resize 不够。
比如:
- 左侧菜单收起
- 顶部标签页显示隐藏
- 搜索区域换行
- 页面容器尺寸变化
- 弹窗内容变化
这些不一定都会触发 window.resize。
所以使用 ResizeObserver 监听相关容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| initTableResizeObserver() { if (typeof ResizeObserver === 'undefined') { return; }
this.destroyTableResizeObserver();
const tableDom = this.getTableWrapper(); const currentDom = this.getCurrentElement(); const pageLayoutDom = currentDom?.closest('.page-layout') || currentDom; const viewScrollDom = currentDom?.closest('.view-scroll');
this.tableResizeObserver = new ResizeObserver(() => { this.scheduleViewHeight(); });
[tableDom, pageLayoutDom, viewScrollDom] .filter(Boolean) .forEach((dom) => this.tableResizeObserver.observe(dom)); }
|
这里重新初始化前要先清理旧的监听,避免 keep-alive 下重复绑定。
重新布局
Element Plus 表格在高度变化或者数据变化后,有时候固定列、滚动条位置会有一点不对。
这时候需要调用 doLayout:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| getTableInstance(refName = 'tableList') { const tableRef = this.$refs[refName]; return Array.isArray(tableRef) ? tableRef[0] : tableRef; }
refreshTableLayout(refName = 'tableList') { if (this.tableLayoutFrame) { cancelAnimationFrame(this.tableLayoutFrame); }
this.$nextTick(() => { this.tableLayoutFrame = requestAnimationFrame(() => { const tableInstance = this.getTableInstance(refName);
tableInstance?.doLayout?.(); this.tableLayoutFrame = null; }); }); }
|
这里单独封装 getTableInstance,是因为有些组件里 ref 可能返回数组。
一般在列表数据请求完成后执行:
1 2 3 4 5 6 7 8
| searchStopLoading() { setTimeout(() => { this.searchLoading = false; this.tableLoading = false; this.scheduleViewHeight(); this.refreshTableLayout(); }, 500); }
|
生命周期
页面第一次进入时,需要初始化监听并计算一次高度:
1 2 3 4 5 6 7 8
| mounted() { this.$nextTick(() => { this.initTableResizeObserver(); this.scheduleViewHeight(); });
window.addEventListener('resize', this.scheduleViewHeight); }
|
如果页面使用了 keep-alive,切换回来时也要重新计算:
1 2 3 4 5 6
| activated() { this.$nextTick(() => { this.initTableResizeObserver(); this.scheduleViewHeight(); }); }
|
离开页面时清理监听:
1 2 3 4 5 6 7 8
| deactivated() { this.destroyTableResizeObserver(); }
beforeUnmount() { this.destroyTableResizeObserver(); window.removeEventListener('resize', this.scheduleViewHeight); }
|
如果页面数据更新后高度可能变化,也可以在 updated 里补一次:
1 2 3 4 5
| updated() { this.$nextTick(() => { this.scheduleViewHeight(); }); }
|
滚动重置
切换分页或者重新查询时,表格滚动条最好回到顶部。
1 2 3 4 5 6 7 8 9 10 11 12 13
| resetTableScroll(refName = 'tableList') { this.$nextTick(() => { const tableInstance = this.getTableInstance(refName); const tableDom = tableInstance?.$el || this.getTableWrapper(); const scrollDoms = tableDom?.querySelectorAll?.('.el-table__body-wrapper .el-scrollbar__wrap, .el-table__body-wrapper');
tableInstance?.setScrollTop?.(0);
scrollDoms?.forEach((dom) => { dom.scrollTop = 0; }); }); }
|
这里除了调用 Element Plus 的 setScrollTop,还兜底处理了内部滚动 DOM。
分页和查询时调用:
1 2 3 4 5 6 7 8 9 10 11
| changePage() { this.resetTableScroll(); this.getTableData(); }
searchTableData() { this.page = 1; this.resetTableScroll(); this.searchStartLoding(); this.getTableData(); }
|
如果不处理,用户在第一页滚动到底部后,再切换到第二页,表格可能还是停在底部。
常见问题
外层还是出现滚动条
优先检查外层高度链路。
一般不是 el-table 的问题,而是某一层容器少了:
1 2 3
| height: 100%; min-height: 0; overflow: hidden;
|
尤其是 flex 布局里面的子元素,min-height: 0 很关键。
搜索条件换行后表格没变
检查是否监听了搜索区域或页面容器尺寸变化。
只监听 window.resize 不够,最好加上 ResizeObserver。
fixed列错位
数据加载完成后调用:
1
| this.refreshTableLayout();
|
如果是弹窗打开后错位,可以在弹窗打开后的 $nextTick 里也调用一次。
总结
这篇主要不是讲动态表格,也不是把表格列配置化。
我的表格列还是按页面固定写,真正抽出来的是高度适配这一块。
整体思路就是:
el-table 使用 :max-height="tableHeight"
- 外层布局先形成完整高度链路
- 根据真实滚动容器高度计算表格可用高度
- 减掉搜索区域、分页和页面 padding 占用的空间
- 弹窗里的表格单独计算高度
- 使用
ResizeObserver 和 requestAnimationFrame
- 数据变化后调用
doLayout
- 查询和分页后重置表格滚动
keep-alive 页面重新激活时重新计算
这样处理以后,不同高度的搜索区域、不同屏幕尺寸、标签页切换、弹窗表格这些场景,都能让表格保持在比较合适的高度。
以上就是我对 Element Plus 表格高度适配的一些理解,如有错误,欢迎大佬指出。