Element Plus表格高度适配


后台系统里面表格页面很多,列配置一般还是固定写在页面里,这里主要记录一下 Element Plus 表格高度自适应的处理

后台项目中,列表页是最常见的页面。

我的表格列并不是动态配置出来的,还是正常在页面里写 el-table-column,这样看起来更直观,后期改某一列也方便。

真正需要统一处理的是表格高度。

因为后台页面经常会有搜索区域、分页、顶部标签页、左侧菜单、浏览器窗口变化等情况,如果表格高度不处理好,就很容易出现下面这些问题:

  1. 页面外层多出一个滚动条
  2. 表格内部也有滚动条,变成双滚动
  3. fixed 固定列错位
  4. 搜索条件换行之后,表格把分页挤出页面
  5. 切换标签页回来之后,高度需要刷新一下才正常
  6. 弹窗里面放表格时,弹窗和表格同时滚动

所以这篇主要记录后台项目里表格高度怎么处理得稳定一点。

页面结构

页面结构大概是这样:

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>

这里主要注意两个地方:

  1. ref="table":外层表格区域,用来计算位置和分页高度
  2. :max-height="tableHeight":Element Plus 表格最大高度

列还是按普通写法写,重点是把 tableHeight 算准确。

如果老项目里面字段已经叫 tableHight 这种拼错的名字,也没必要为了一个变量名到处改,保持当前项目能稳定运行更重要。

为什么用max-height

Element Plus 的表格如果不限制高度,数据多的时候会直接把页面撑开。

后台页面一般不希望整页一直往下滚,而是希望:

  1. 搜索区域固定在上面
  2. 分页固定在表格下面
  3. 表格内容区域内部滚动
  4. 外层布局不要出现多余滚动条

所以这里使用 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
};
}

其中:

  1. tableHeight:最终给 el-table 使用的高度
  2. marginHeight:没有分页时预留的底部高度
  3. tableResizeObserver:监听布局尺寸变化
  4. tableHeightFrame:控制高度计算的节流
  5. 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

大概思路是:

  1. 获取外层滚动容器高度
  2. 获取表格区域距离容器顶部的距离
  3. 获取分页高度
  4. 减去页面底部 padding
  5. 得到表格可用高度
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;

这样搜索区域变高或者变低,表格高度都能跟着变化。

弹窗里的表格

还有一种情况比较容易出问题:弹窗里面放表格。

如果继续按页面列表的逻辑算高度,弹窗经常会变成:

  1. 弹窗外层滚动
  2. 表格内部滚动
  3. 页面底部也跟着滚

这种体验很别扭。

所以我会在 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 不够。

比如:

  1. 左侧菜单收起
  2. 顶部标签页显示隐藏
  3. 搜索区域换行
  4. 页面容器尺寸变化
  5. 弹窗内容变化

这些不一定都会触发 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 里也调用一次。

总结

这篇主要不是讲动态表格,也不是把表格列配置化。

我的表格列还是按页面固定写,真正抽出来的是高度适配这一块。

整体思路就是:

  1. el-table 使用 :max-height="tableHeight"
  2. 外层布局先形成完整高度链路
  3. 根据真实滚动容器高度计算表格可用高度
  4. 减掉搜索区域、分页和页面 padding 占用的空间
  5. 弹窗里的表格单独计算高度
  6. 使用 ResizeObserverrequestAnimationFrame
  7. 数据变化后调用 doLayout
  8. 查询和分页后重置表格滚动
  9. keep-alive 页面重新激活时重新计算

这样处理以后,不同高度的搜索区域、不同屏幕尺寸、标签页切换、弹窗表格这些场景,都能让表格保持在比较合适的高度。

以上就是我对 Element Plus 表格高度适配的一些理解,如有错误,欢迎大佬指出。

-------------本文结束感谢您的阅读-------------