背景
系统有个整改需求,要求系统内的所有表格支持本地动态列显隐,拖拽排序列位置,固定列功能,涉及的页面很多
上效果图:
思路
其实最开始想的肯定是json配置表单的形式,再由循环出来的列去控制对应的位置和属性 但是!很多页面啊!每个页面都要去转json配置意味着大量的工作量和极高的风险
能不能我就写个自己的组件来包一层,这样我就能实现最小改动的情况下只需要替换组件标签来实现这个功能
与实际的不同只是我将原来的el-table换成了hf-table,同时支持原本el-table的所有功能
想法与实践
el-table-column获取
我们不可能去自己实现一个el-table的组件,所以无非我们的组件就是在el-table的基础上套一层壳,给他加上一个设置按钮,同时设置的内容能够去影响整个表格的渲染。
那既然我们不自己实现el-table则意味着原先代码中的el-table-column我们要拿到,并且要传给el-table,这样我们才能去渲染出来原先的那个表格
在一个组件的实例中,我们能够通过 vnode有一个componentOptions组件配置项的属性,通过他的children就能获取到所有的el-table-column 虚拟dom数组
如何渲染表格
上一步我们已经拿到了所有的el-table-column虚拟dom,那怎么将虚拟dom去渲染成对应的表格组件呢?
这不render就该登场了吗!!
这个children就是我们拿到的el-table-column的数组,我们只需要将该虚拟dom的数组以组件属性的形式传传进来了,再创建一个el-table,将对应的children传给他!卧槽,这不就又和原本<el-table>xxx</el-table>
的效果一毛一样吗,是的 ,我做的就是挂羊头卖狗肉。
也就是说,实际上我的hf-table只是劫持了el-table,他的作用只是拿到原本写的el-table-colunm的虚拟dom,去渲染成一个表格
操作表格
此时我们的任务已经完成大半了,就是我原本el-table的标签已经可以被替换了,那我们要做的就只剩下操作表格了。 实际我做的很简单,既然我已经拿到了所有的子节点,那我就在hf-table组件中去操作成我想要的数组,再丢给render函数去渲染就好了
组件代码
整个组件的代码,代码量除掉样式也就不到100行
<template>
<div class="hf-table">
<el-popover
placement="bottom-end"
width="400"
popper-class="table-cloumn-setting-popper"
trigger="click"
>
<div class="setting-row-content">
<draggable v-model="storageList" handle=".el-icon-s-operation" @end="updateTable">
<div v-for="clo in storageList" :key="clo.label" class="setting-row">
<i class="el-icon-s-operation" />
<el-checkbox v-model="clo.show" class="label" @change="showOrHidden($event,clo)">{{ clo.label }}</el-checkbox>
<el-button
class="btn"
size="mini"
:type="clo.fixed === 'left' ? 'primary' : 'default'"
@click="setFixed('left',clo)"
>固定在左侧</el-button>
<el-button
class="btn"
size="mini"
:type="clo.fixed === 'right' ? 'primary' : 'default'"
@click="setFixed('right',clo)"
>固定在右侧</el-button>
</div>
</draggable>
</div>
<i slot="reference" class="el-icon-setting" />
</el-popover>
<new-table v-if="showTable" :config="config" />
</div>
</template>
<script>
import draggable from 'vuedraggable'
import newTable from './table.js'
const components = { newTable, draggable }
export default {
components,
props: {
storageName: {
type: String,
default: 'hfTable'
}
},
data() {
return {
showTable: false,
storageList: [],
name: '',
config: {
children: [],
attrs: {},
listeners: {}
}
}
},
watch: {
'$attrs': {
handler(newV) {
this.$set(this.config, 'attrs', newV)
},
deep: true,
immediate: true
}
},
mounted() {
this.initStorage()
this.updateTable()
},
methods: {
showOrHidden(val, clo) {
if (!val && this.storageList.filter(i => i.show).length === 0) {
this.$message.warning('列表最少显示一列')
this.$nextTick(() => {
clo.show = true
})
return
}
this.updateTable()
},
setFixed(value, clo) {
if (clo.fixed === value) {
clo.fixed = false
} else {
clo.fixed = value
}
this.updateTable()
},
// 初始化缓存配置
initStorage() {
this.storageList = []
const storage = window.localStorage.getItem(this.storageName)
// 不管是否初次还是要做一下处理,万一页面有修改,做一下更新,以最新的node节点数组为准
let list = storage ? JSON.parse(storage) : []
this.$vnode.componentOptions.children.forEach(node => {
// 以label为准,因为可能会改文本
if (!node.componentOptions.propsData.type && list.findIndex(i => i.label === node.componentOptions.propsData.label) < 0) {
// 不是特殊类型的 找不到就加上
const propsData = JSON.parse(JSON.stringify(node.componentOptions.propsData))
propsData.fixed = propsData.fixed !== undefined ? 'left' : false
list.push({
fixed: false, // 默认新增的都是不固定
show: true, // 默认新增的都是显示的
...propsData
})
}
})
// 必须在节点数组存在的才有意义
list = list.filter(item => this.$vnode.componentOptions.children.find(n => {
return item.label === n.componentOptions.propsData.label
}))
this.storageList = list
},
// 根据缓存的数组进行渲染表格
updateTable() {
const childrenNodes = this.$vnode.componentOptions.children.filter(node => node.componentOptions.propsData.type)
this.storageList.forEach(item => {
if (item.show) {
const node = this.$vnode.componentOptions.children.find(n => n.componentOptions.propsData.label === item.label)
if (node) {
node.componentOptions.propsData.fixed = item.fixed
childrenNodes.push(node)
}
}
})
this.config.children = childrenNodes
this.config.attrs = this.$attrs
this.config.listeners = this.$listeners
this.showTable = false
this.$nextTick(() => {
this.showTable = true
})
window.localStorage.setItem(this.storageName, JSON.stringify(this.storageList))
}
}
}
</script>
<style lang="scss" scoped>
.table-cloumn-setting-popper{
.setting-row-content{
max-height: 600px;
overflow-y: auto;
.setting-row{
height: 40px;
line-height: 40px;
.el-icon-s-operation{
cursor: move;
font-size: 16px;
margin-right: 8px;
}
.label{
margin-right: 8px;
}
.btn{
padding: 4px!important;
}
}
}
}
.hf-table{
width:100%;
height:100%;
position: relative;
.el-icon-setting{
position: absolute;
right: 20px;
top:-20px;
cursor: pointer;
}
}
</style>
复制代码
表格函数式组件
import Vue from 'vue'
export default Vue.component('newtable', {
functional: true,
props: {},
listeners: {},
render: function(h, context) {
return h(
'el-table',
{
props: context.data.attrs.config.attrs,
on: context.data.attrs.config.listeners
},
context.data.attrs.config.children
)
}
})
复制代码