1. 项目背景
最近产品那边来了个需求,业务同事在截图汇报时希望能一张图截完全部必要的字段,本身要截图的字段并不多,但是由于有时候表格列过多,导致某些字段需要滚动到后面才能看到,因此希望在截图时只展示表格必要的那几列,其它不需要的列隐藏。
产品最初的需求是针对某个业务高频调整的特定的表格进行改动,在表格右上角添加一个图标,点击后弹出浮层可以勾选需要哪些列,就像下面这样:
而为了通用,我们最终决定做成全局的功能。
2. 功能设计
我们的项目是 Vue 2.7 + ElementUI
,历经了多年多人的维护,项目非常庞大,如果去每个表格改动代码必然是不现实的,因此需要考虑一种比较通用的方式来实现。
最容易想到的是通过 DOM API 来实现,通过手动创建一个元素添加在表格右上角,监听该元素的事件并操作表格 DOM 从而达到视觉上的表格过滤效果,但手动去修改表格列的这种方式并不优雅,而且容易出错 ------ 因为 ElementUI
表格并不是一个单纯的表格实现,而是根据参数不同而叠加多个表格来实现功能的,例如当你打开浏览器 DevTools
查看时,你可以看到表头是一个 <table>
,表身是一个 <table>
,如果有固定列,固定列又是单独的 <table>
元素,且需要考虑的因素过多(例如移除 DOM 后如果再次勾选需要还原等),排除此方式。
既然是 MVVM
框架,我们肯定是希望通过数据来驱动视图发生变化,如果能这样,那就相当于把维护 DOM 结构的事情交还给了 ElementUI
,我们不用再考虑 DOM 的各种结构差异和一系列的问题。
因此我决定通过 Vue Mixin
的方式来实现。
为了实现上面的效果,我们需要做以下几个事情:
- 创建一个用于勾选列的弹出层,暂且称它为
FilterBar
- 表格挂载到 DOM 的时候,将
FilterBar
挂载到表格右上角 - 将列数据传递给
FilterBar
,并监听FilterBar
抛出的事件 - 根据
FilterBar
传递的数据,显示或隐藏表格列
3. 实现
我们创建一个 mixins/FilterBar
目录用于存放该功能的代码,整理目录结构如下:
其中:
FilterBar.vue
用于弹出层的组件实现index.js
用于实现该 mixin 的主要功能style.less
用于简单的样式管理
3.1 创建 FilterBar
先基于 el-popover
创建一个非常简单的 FilterBar
组件,主要用于展示列选项并支持勾选,每当勾选发生变化时就向外抛出 filter
事件通知外部。
jsx
<template>
<div>
<el-popover
placement="bottom"
:append-to-body="true"
v-model="searchFromPopoverVisible"
trigger="click"
>
<i
class="el-icon-menu"
slot="reference"
/>
<div>
<el-checkbox-group v-model="selectedColumns">
<el-checkbox
v-for="column in columns"
:label="column.label"
:key="column.label"
:name="column.label"
>
{{ column.label }}
</el-checkbox>
</el-checkbox-group>
</div>
</el-popover>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
const emit = defineEmits(["filter"]);
const searchFromPopoverVisible = ref(false);
const columns = ref([]);
const selectedColumns = ref([]);
watch(selectedColumns, (newVal) => emit("filter", newVal));
defineExpose({
columns,
});
</script>
3.2 创建 Mixin
3.2.1 创建并挂载 FilterBar
在 Mixin 逻辑中,我们首先在 mounted
时挂载 FilterBar
,并在监听到 filter
事件时执行对表格列的过滤操作:
js
import Vue from "vue";
import { Table } from "element-ui";
import FilterBar from "./FilterBar.vue";
const FilterBarComponent = Vue.extend(FilterBar);
Table.mixins?.push({
mounted() {
// 创建一个元素用于挂载图标
const icon = document.createElement("div");
// 创建 FilterBar 组件
const filterBar = new FilterBarComponent();
// 将表格列数据写到 FilterBar 组件中
filterBar.columns = this.store.states._columns;
// 监听过滤事件
filterBar.$on("filter", (column) => {
this.filterColumn(column)
});
if (this.$el) {
// 添加组件到 el-table 内
this.$el.appendChild(icon);
filterBar.$mount(icon);
}
},
methods: {
async filterColumn(column) {
// TODO: 过滤逻辑
},
},
});
3.2.2 实现过滤逻辑
要实现过滤逻辑就需要简单的看一下 el-table
内部是如何实现的,并直接调用它内部的 API,这里简要阐述一下:
el-table
内部更新列的方法是 updateColumns
,我们可以通过 this.store.updateColumns()
来更新列。更新完列之后,还需要调用 doLayout
来重新布局表格,否则可能会出现样式问题。
另外,由于 el-table
内部的列数据是通过 this.store.states._columns
来存储的,通过 v-if
控制的那些动态列发生变化也会被 el-table
内部更新到 _columns
中去,updateColumns
也是参照 _columns
来对表格列进行更新,因此只要修改 _columns
的值然后调用 updateColumns()
就能够让 el-table
自动更新表格数据,基于此我们来实现一个简要的过滤逻辑:
js
{
methods:{
async filterColumn(column) {
this.store.states._columns = this.store.states._columns.filter((col) => column?.includes(col.label),);
this.store.updateColumns();
await nextTick(); // 等待输完更新完成再 layout,如果同步 layout 可能出现样式问题(如正常的列和 fixed 的列高度出现不一致等情况)
this.doLayout();
},
}
}
4. 总结
以上便是一个用于对 el-table
列进行过滤的简要核心逻辑,当我们在项目中引入该 Mixin 即可进行对表格列的过滤操作了,但仍然有一些细节需要处理,例如我们只在一开始将表格数据写入了 FilterBar
组件中,那些通过数据和 v-if
进行动态控制的列就不会同步到 FilterBar
中去,考虑到这些细节问题并不太适合在文中赘述,我将完整代码放到了 GitHub 上,希望能对各位有一定帮助,有兴趣的同学可以去看看。
效果图片: