小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
前言
大家好,最近我在做一个PC端基于element-ui封装一个上层的表格的组件,组件涵盖了表格的大部分相关功能,比如: 基础表格、分页、查询、动态表头等,功能的话,我是边构思,边写代码,然后在把思路和代码写成文章发到掘金的。 对这个组件有兴趣的,可以从我之前的第一篇写此组件的文章开始看。 这篇文章主要是写动态表头的实现. 主要的效果如下:
因为表格支持多表头的功能,所以我们需要对动态表头做如下功能:
- 实现显示隐藏一级表头、二级表头、三级表头
- 实现表头查询功能
- 实现表头拖拽排序功能 以上是需要实现的功能,在明确需求后,会发现element-ui的树组件完美的满足我们的需求. 于是我们的动态表头就可以基于tree组件去实现.
基础功能
那么我们现在就开始把基础功能给实现以下吧:
// 新建组件: columnSetting.vue
<el-dialog
class="columnConfig"
:modal-append-to-body="true"
:append-to-body="true"
@close="closeColumnConfig"
:close-on-click-modal="false"
ref="columnConfigDialog"
title="动态表头配置"
:visible.sync="columnConfigDialog"
width="600px"
>
<el-tree
ref="tree"
:data="columns"
show-checkbox
node-key="prop"
default-expand-all
:default-checked-keys="checkValues"
:props="defaultProps">
</el-tree>
<div align="center" style="margin-top: 20px;clear: both;">
<el-button size="small" :loading="save_loading" type="primary" @click="save">保存</el-button>
<el-button size="small" type="danger" @click="cancel">取消</el-button>
</div>
</el-dialog>
// data
defaultProps: {
children: '__children',
label: 'label'
}, // tree组件显示字段和子数据的字段
save_loading: false, // 保存loading
columnConfigDialog: false, // 窗口显示隐藏开关
columns: [], // 树的数据
checkValues: [] // 默认选中的数据
以上大致的需要的变量和结构都定义好了. 然后我们再写逻辑的部分. 首先我们这边写一个打开窗口的方法,给父组件调用,其中这里个人建议,如果是需要给其他组件调用的方法, 我们在命名上可以特殊一点,比如我这里给这种父组件调用的方法加上一个前缀j_,这样的话,后边如果我们做代码优化,把一些冗余的方法删除的时候,就会注意到这个j_开头的方法,并不是在本组件调用,而是其他组件调用内部的方法. 从而避免错误删除掉有用的东西.
// methods
// 打开窗口
j_showDialog(columns) {
this.checkValues = [] // 清空默认选中的数据
this.initColumns(columns) // 如果第一次传递过来的是我们在写组件定义的列的数据,如果设置过动态表头,传递过来的就是,缓存的动态表头配置.
this.columnConfigDialog = true // 打开窗口
this.columns = columns // 通过initColumns设置默认选中的checkValues
}
// 初始化列
initColumns (columns) {
columns.forEach((o) => {
// 第一次进来传递过来的是列的数据,所以checked是undefined,对于第一次进来的数据,默认给选中,因为它默认就展示在表格上了.
if (o.checked || o.checked === undefined) {
this.checkValues.push(o.prop)
}
// 递归子数据赋值checked
if (o.__children && o.__children.length) {
// 如果有子数据,则递归
this.initColumns(o.__children)
}
})
},
以上代码通过递归columns的数据,拿到默认需要选中的数据然后赋值给checkValues,传递给tree组件。 这样打开窗口,页面就会正常显示树的数据,并自动勾选上了。 接着我们需要在勾选完想要的数据后,点击保存按钮
// methods
// 保存
save () {
let checkValues = this.$refs.tree.getCheckedKeys() // 拿到选中的数据的key
this.setColumn(this.columns, checkValues) // 通过递归setColumn方法,给每一个数据添加checked属性. 选中了true, 没选中false
this.$emit('columns', JSON.parse(JSON.stringify(this.columns))) // 把修改好的数据,传递给父组件
setCache(this.name, this.columns) // 本地缓存起来列的数据, name是用户传递到组件来的名称
this.cancel() // 关闭页面
},
// 设置是否选中属性checked
setColumn (columns, checkValues) {
columns.forEach((o) => {
o.checked = !!~checkValues.indexOf(o.prop)
if (o.__children && o.__children.length) {
this.setColumn(o.__children, checkValues)
}
})
},
保存完数据后,设置的动态表头就传递到了我们封装的mini-table组件了.
let myColumn = getCache('columns') // 取缓存的动态表头数据
if (myColumn) {
this.filterColumns(myColumn) // 这个方法后边会将,主要把数据再做一层处理,添加checked属性
this.myColumn = myColumn // 赋值给表格的列数据
} else {
// 没有设置过动态表头的话,则把用户传递的列数据的副本给表格的列数据, 因为我们需要对myColumn的数据做一些修改,所以不能直接操作用户通过props传递给组件的数据.
this.myColumn = this.columns.slice()
}
在mini-table组件里边的mounted里边,我们先去缓存里边取动态表头,没有的话,则取用户设置的列数据, 接着前面我们在columnSetting.vue组件里边抛出了一个自定义事件columns, 然后通过这个事件拿到用户设置好的动态表头数据,再做一层处理.
<column-setting ref="columnSetting" @columns="setColumns"></column-setting>
// methods
async setColumns (columns) {
this.filterColumns(columns) // 传递过来的数据需要做一下处理。
this.myColumn = columns // 把设置过一遍的数据给赋值到table组件的列数据里边设置显示的列
this.key = Symbol() // 让组件重新渲染一下
}
// 由于传递过来的数据,它的子数据是checked=true,但是它的父节点还是checked还是false, 我们通过递归判断父节点的checked如果是false的话,再去判断一下子节点的数据有没有checked是true的,如果有的话,则把它的父节点也设置成true.
filterColumns (columns) {
let checked = false
columns.forEach((o) => {
if (!o.checked) {
if (o.__children && o.__children.length) {
if (o.__children.find((item) => {
return item.checked === true
})) {
o.checked = true
checked = true
this.filterColumns(o.__children)
} else {
o.checked = this.filterColumns(o.__children)
}
} else {
o.checked = false
}
}
})
return checked
}
最后我们在表格中判断如果节点的checked不为false的话,则显示出来,否则不显示
<el-table
v-loading="loading"
ref="__table"
:data="tableData"
v-bind="$attrs"
:row-key="idKey"
v-on="listeners"
:key="key"
>
<el-table-column
v-if="isCheck"
align="center"
key="__checkColumn"
width="70"
type="selection"
:reserve-selection="isCheckMemory"
>
</el-table-column>
<el-table-column
v-if="isIndex"
key="__indexColumn"
show-overflow-tooltip
align="center"
:index="typeIndex"
type="index"
:fixed="fixed"
>
<template slot="header">
// 在这里设置一个齿轮点击弹出动态表头的窗口
<span>序号<span @click="setting" class="icon el-icon-setting"></span></span>
</template>
</el-table-column>
<template v-for="item in myColumn">
<my-table-column // 这个组件内部也需要再判断一下多表头里边的子表头的checked是否不为false
v-if="item.__children && item.checked !== false" // <===== 这里判断checked
:item="item"
:sortArr="sortArr"
:key="item.prop"
></my-table-column>
<el-table-column
v-else-if="item.checked !== false" // <===== 这里判断checked
:key="item.prop"
v-bind="item"
:sortable="sortFun(item.prop) ? 'custom' : false"
show-overflow-tooltip
>
<template v-if="item.__slotName" v-slot="scope">
<slot :name="item.__slotName" :data="scope"></slot>
</template>
<template v-else-if="item.__render" v-slot="scope">
<slot-ext
:render="item.__render"
:index="scope.$index"
:column="item"
:row="scope.row"
></slot-ext>
</template>
</el-table-column>
</template>
</el-table>
我们把动态表头触发放到序号表头的齿轮上设置,点击触发
// 打开动态表头
setting () {
let columns = getCache('columns') || this.columns // 缓存有数据从缓存取,没有则取用户设置的表格的列数据
this.$refs.columnSetting && this.$refs.columnSetting.j_showDialog(columns, this.name)
},
那么,至此基础的功能就实现了。 里边的一些工具方法,比如setCache、getCache其实只是做了简单的一层封装
// 设置缓存数据
export const setCache = function (key, value) {
if (typeof value === 'object' && value != null) {
value = JSON.stringify(value)
}
localStorage.setItem(key, value)
}
// 获取缓存数据
export const getCache = function (key) {
let val = localStorage.getItem(key)
try {
return JSON.parse(val)
} catch (e) {
return val
}
}
过滤
实现过滤方法就很简单了。 输入框输入数据后,监听到数据变化, 然后调用tree的filter方法传入输入的值.然后通过树组件提供了filter-node-method,参数value是输入的数据,data是树节点的数据. 直接用indexOf过滤出来匹配的就好了。
// template
<el-input
class="filter"
size="small"
placeholder="输入关键字进行过滤"
v-model="filterText">
</el-input>
<el-tree
ref="tree"
:filter-node-method="filterNode"
:data="columns"
show-checkbox
node-key="prop"
default-expand-all
:default-checked-keys="checkValues"
:props="defaultProps">
</el-tree>
// watch
watch: {
filterText(val) {
this.$refs.tree.filter(val);
}
},
// methods
// 过滤方法
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
拖拽排序
拖拽排序,也是树组件自带的,我们在他这上面定义规则,比如我们希望拖拽排序只能同级间拖拽排序,并且不能拖拽到同级的内部,作为它的子表头.
<el-tree
ref="tree"
draggable
:data="columns"
:filter-node-method="filterNode"
show-checkbox
node-key="prop"
default-expand-all
:default-checked-keys="checkValues"
:props="defaultProps"
:allow-drop="allowDrop"
:allow-drag="allowDrag">
</el-tree>
实际只要在tree组件上加入draggable属性,再加上allow-drop和allow-drag. 其中
- allowDrag 表示哪些节点可以拖动
- allowDrop 表示可以放置在哪个节点前面或者后面
// 拖拽方法
allowDrop(draggingNode, dropNode, type) {
// 只能同级拖拽排序,不能拖到同级的内部去
if (draggingNode.level === dropNode.level && type !== 'inner') {
return true
}
},
// 设置每个节点都可以拖拽
allowDrag() {
return true
},
ok, 完成了。以下看一下拖拽和排序的效果
写在最后
那么,基本上表格及表格周边的功能都覆盖到了。 以后在项目中碰到一些比较通用的功能,还会继续加进来。 更好的完善这个组件. 让它能更好用! 如果对这个表格组件感兴趣的话,可以点这里我已经把它开源出来了。 欢迎Star, 也欢迎大家对本文章点赞、关注一波~ 路漫漫,大家一起进步!