功能列表
- 鼠标在表头文字区域按压、拖拽,调整列序;
- 根据个人需要自定义各列的显隐;
- 一键恢复初始列序、列宽、显隐;
- 本地保存列序、列宽、显隐等记忆数据,不受刷新影响;
- 系统内各表格的记忆数据互不干扰,和谐共存。
体验demo:https://app.wy310.cn/drag-table/#/
一、自定义列序
1. 如何选型?
Element对el-table
做了严谨的封装,如果以操作dom的方式来改变列序可能会出现很多意想不到的问题(这类方法可能会用到sortable.js
或vuedragable
库)。而vue的思想是数据驱动视图,为了顺应这种思想,在组件开发初期的选型阶段,采用表头数据JSON化的方式来达到自定义列序的目的:获取表格各列数据,构造成一个数组,使用v-for遍历该数组,通过改变数组中某个元素的索引来改变表格列的顺序。
2. 对原代码的修改代价如何?
<drag-table>
<el-table-column label="ID" prop="id" fixed="left"></el-table-column>
<el-table-column label="姓名" prop="name"></el-table-column>
<el-table-column label="年龄" prop="age"></el-table-column>
<el-table-column label="性别" prop="gender"></el-table-column>
...
<el-table-column label="操作" prop="ctrl" fixed="right"></el-table-column>
</drag-table>
我们发现,使用自定义列序的drag-table
组件对原代码的修改代价是很小的,首先得换个“壳”,这个“壳”就是drag-table
头尾标签,而内部的el-table-column
是不需要怎么改动的,这里我还不敢说完全不用改,是因为:
- 我们还需要确保所有字段都具备
prop
属性(localStorage里需要存储各字段的prop
)
drag-table.vue
内部结构:
<el-table ref="dragTable" class="drag-table" :class="{ 'drag-table_moving': dragState.dragging }" v-bind="$attrs" v-on="$listeners" :data="data" :cell-class-name="cellClassName" :header-cell-class-name="headerCellClassName" @header-dragend="headerDragend" style="width: 100%">
<dd-table-column v-for="(col, idx) in visibleColumn" :key="idx" v-bind="col" :column-key="idx.toString()" :mousedown="handleMouseDown" :mousemove="handleMouseMove" :is-last-column="idx === visibleColumn.length - 1" @customDisplay="handleDisplay"></dTableColumn>
</el-table>
drag-table
基于el-table
封装,内部用v-for循环遍历可见列(visibleColumn
)。
dd-table-column
暂时理解成el-table-column
就好了,后面会详细讲。
column-key
绑定数组的idx
,用于确定需要修改的数组元素下标。
v-bind="col"
是vue关于简便列举元素属性的语法糖,可避免一一列举所有属性:
...
:label="col.label"
:prop="col.prop"
:width="col.width"
:fixed="col.fixed"
...
3. visibleColumn数据如何获取?
visibleColumn = allColumn - hide
首先需要获取所有表格列——allColumn
,再剔除hide === true
的列,剩下的就是可见列——visibleColumn
。
// 获取所有表格列(template里显式书写的el-table-column列表)
getAllColumn () {
const slotDefault = this.$slots.default.filter(v => v.tag).slice(0)
const arr = []
for (let i = 0; i < slotDefault.length; i++) {
const vnode = slotDefault[i]
const propsData = vnode.componentOptions.propsData
propsData.cellType = 'slots'
// 手动为'expand', 'selection', 'index'三类特殊列设置prop,值就是其type
if (propsData.type) {
propsData.prop = propsData.type
}
const scopedSlots = vnode.data.scopedSlots
const renderCell = scopedSlots && scopedSlots.default ? scopedSlots.default : scope => <span>{scope.row[propsData.prop]}</span>
arr.push({ ...propsData, renderCell })
}
return arr
},
initData() {
...
if (!columnLocal.length) {
// 该页还没有记忆数据
this.visibleColumn = this.allColumn;
} else {
// 该页已有记忆数据
const visibleColumn = [];
// 经过筛选和排序操作,得到visibleColumn
...
this.visibleColumn = visibleColumn;
}
},
4. visibleColumn数据有哪些?
通过$slots.default
可以获取表格内部(确切的说是默认插槽里的)所有el-table-column
的vnode。从vnode里,我们可以得到一些有用的数据:
componentOptions.propsData
(列属性、表头渲染器)data.scopedSlots.default
(单元格渲染器)
列属性被v-bind="col"
绑定在各列上,提供了各列需要的label
、prop
、sortable
、width
、fixed
等属性;
表头渲染器在dd-table-column
中被赋值给columnConfig.renderHeader
,实现了自定义表头的渲染。
单元格渲染器在dd-table-column
中被赋值给columnConfig.renderCell
,实现了自定义内容的渲染。
5. 如何实现拖拽改变列序?
在各列的表头中的名为header的插槽中创建一个div.thead-cell
,监听其上的mousedown
、mousemove
、mouseup
事件,mousedown
时记录当前列的索引——start,mouseup
时记录最终悬浮列的索引——end,根据start、end这两个索引,经过易位计算,最后得出最新的数组,通过vue的双向绑定,即完成了表格列序的调整。
.thead-cell
上绑定了鼠标事件,鼠标按下后,.virtual
被赋予各列的宽度,它是透明的,是为了显示左、右虚线border
而存在的,这样能给用户传达一个视觉信号——你将要把所选列落在这里。
为了实现拖拽的交互效果,需要在el-table
标签上征用两个属性:
header-cell-class-name
绑定一个函数,动态给表头单元格添加 class,从而实现拖动中的虚线效果。cell-class-name
绑定一个函数,动态给表体单元格添加 class,用于区分选中列。
6. 表格数据JSON化还会带来哪些问题?
第3步只说了如何获取renderCell
、renderHeader
,但如何渲染还未解决:
- 单元格如何渲染自定义的内容?
- 表头如何渲染自定义的内容?
<!-- 单元格内插入一个按钮 -->
<el-table-column prop="id" label="ID">
<template slot-scope="scope">
<el-button type="text">{{ scope.row.id }}</el-button>
</template>
</el-table-column>
<!-- 表头尾部插入一个提示图标 -->
<el-table-column label="年龄" prop="age" :render-header="renderHeader"></el-table-column>
// methods
renderHeader(h, { column, $index }) {
return [
'年龄',
h(
'el-tooltip',
{
props: {
content: (() => {
return '一个人从出生时起到计算时止生存的时间长度,通常用年岁来表示';
})(),
placement: 'top-start',
},
},
[
h('span', {
style: 'margin-left: 5px;',
class: {
'el-icon-info': true,
},
}),
]
),
];
},
为了解决上面两个问题,需要创建一个dd-table-column.vue
,它继承自el-table-column
,重写renderCell
、renderHeader
函数改变其返回内容。
<script>
import { TableColumn } from 'element-ui'
const renderCell = {
slots (h, data) {
// 接受传入的renderCell函数
const renderCell = (h, data) => {
return this.renderCell ? this.renderCell(data) : ''
}
return <div class="cell">{renderCell(h, data)}</div>
}
// 其他类型暂未扩展
}
export default {
extends: TableColumn, // 继承el-table-column
props: {
cellType: {
type: String,
validator (value) {
const valid = ['slots'].includes(value)
!valid && console.error(`dd-table-cloumn组件不适配 ${value} 类型,适配类型有'slots'`)
return valid
}
},
renderCell: Function,
mousedown: Function,
mousemove: Function,
isLastColumn: Boolean
},
created () {
// 对于'expand', 'selection', 'index'三类特殊列不执行自定义渲染,使用原生的功能
if (['expand', 'selection', 'index'].includes(this.type)) return
if (renderCell[this.cellType]) {
this.columnConfig.renderCell = renderCell[this.cellType].bind(this)
}
this.columnConfig.renderHeader = (h, { column, $index }) => {
const headContent = this.renderHeader ? this.renderHeader(h, { column, $index }) : column.label
return h(
'div',
{
// 在.thead-cell上绑定鼠标按下和移动事件,fixed-cell用于展示禁止落点的鼠标图标
class: ['thead-cell', column.fixed ? 'fixed-cell' : '', this.isLastColumn ? 'last-column' : ''],
on: {
mousedown: $event => {
this.mousedown($event, column, $index)
},
mousemove: $event => {
this.mousemove($event, column, $index)
}
}
},
[
// 添加 <a> 用于显示表头,其上添加stopPropagation()上是为了让表头的按钮点击事件禁止冒泡(否则可能会触发el-table的排序)
column.fixed
? h('span', {}, [headContent])
: h(
'a',
{
on: {
click: e => {
e.stopPropagation()
}
}
},
[headContent]
),
// 为非冻结列添加一个空标签用于显示拖动中的目标位置虚线
column.fixed
? ''
: h('span', {
class: ['virtual']
}),
// 为最后一列添加自定义显示的编辑按钮
this.isLastColumn
? h('span', {
class: 'el-icon-edit',
on: {
click: e => {
e.stopPropagation()
this.$emit('customDisplay')
}
}
})
: ''
]
)
}
}
}
</script>
二、记忆列宽
el-table
提供了一个事件:@header-dragend
,当拖动表头改变了列的宽度的时候会触发该事件,参数为newWidth
、oldWidth
、column
、event
。
我们只需要在el-table
上监听该事件,在事件内部,把newWidth
赋给数组中指定索引的元素的width
即可。
headerDragend (newWidth, oldWidth, column) {
const arr = this.visibleColumn
const idx = arr.findIndex(v => v.prop === column.property)
const value = { ...arr[idx], width: newWidth.toString() }
this.$set(this.visibleColumn, idx, value)
},
三、自定义显示解耦
前期,drag-table
组件的自定义显示是和筛选项混在一起的,这就导致了一个问题,局限了drag-table
的使用场景——只能在每个模块的首页使用。要让它能在详情页或是任何地方使用,需要把表格列的自定义显示功能(下图右半部分)抽离出来,封装进drag-table
组件内部,这样,在任何地方,只要点击最后一列表头的编辑图标,就可调出自定义显示弹框。
改写drag-table.vue
,将包裹了custom-display
(自定义显示)的dialog
放进组件内部:
<template>
<div class="table-wrapper">
<el-table ...>
<dd-table-column ...></dd-table-column>
</el-table>
<el-dialog ...>
<custom-display ...></custom-display>
</el-dialog>
</div>
</template>
四、本地存储
1. 拖拽表格列改变列序(存入)
拖拽后需要把新数据存入localStorage
,目的是保存用户的当前习惯,这样在下次刷新时还可以恢复当前列序。
注意:每次须按照【左冻结 – 显示 – 隐藏 – 右冻结】的顺序存入localStorage
,这样每次取出时就不再需要排序,也能保证渲染出来的效果是正确的。
不同页面的表格怎么存储呢?
——以各页面的路由名称为key
保存进localStorage
。
那同一个页面的多个表格又如何存储而不互相干扰呢?
——drag-table
组件提供了一个名为address
的prop
,用于区分不同位置的表格,比如<drag-table address="detail"></drag-table>
就表示某页面下的详情页的表格,组件内部会以“页面路由_address”的方式拼接起来作为key
存入localStorage
,比如:page1_detail
。
相关代码:drag-table.vue的saveData()
2. 刷新页面恢复记忆列序(取出)
刷新页面后,在mounted
钩子中获取allColumn
,再获取localStorage
里的column
,按照“使用column的顺序、列宽和显隐属性,使用allColumn的字段内容”的规则得到最终的visiableColumn
,再通过v-for
将其遍历渲染出来,改变后的列序就这样被恢复了。
这部分功能其实也是下面的4.2、5.2、6.2。
相关代码:drag-table.vue的getAllColumn()和initData()
3. 打开自定义显示弹框(取出)
表格最后一列的表头右侧,有一个可以打开自定义显示弹框的按钮:
为了能将各字段复选框的顺序正确的渲染出来,需要获取localStorage
里的column
,筛选那些无hide的字段,赋值给columnList
。
相关代码:custom-display.vue的initData()
4. 自定义显示设置显隐(存入 + 取出)
勾选自定义显示弹框中的复选框,点击确认按钮,程序根据用户的勾选状态设置每个列的hide
字段(为隐藏的列添加hide: true
),然后存入localStorage
,这是图中 4.1 部分的存入。
相关代码:custom-display.vue的submitData()
以上只完成了一半,即数据的存入,要想表格正确渲染用户的勾选结果,还需要从localStorage
里取出、计算,才能将最终结果表达出来,也就是上面介绍过的 2. 刷新页面恢复记忆列序(取出)的部分,不再赘述。
5. 自定义显示拖拽排序(存入 + 取出)
自定义显示弹框除了可以设置显隐,还能进行拖拽排序,这里是通过vuedragable
库(一款支持拖拽的vue插件,支持移动设备、拖拽和选择文本、智能滚动等场景)实现的。拖拽之后会生成一份新序数据,将这部分数据存入localStorage
,即图中5.1部分。
相关代码:custom-display.vue的submitData()
取出部分同 2。
6. 自定义显示一键恢复(存入 + 取出)
有些时候,如果用户在经历一番显隐、拖拽操作后想回到初始状态怎么办?drag-table
提供了一键恢复功能,入口在自定义显示弹框的右上角,点击它会执行一遍初始化操作,重新获取表格的allColumn
(通过$slots.default
的方式),然后再得到visiableColumn
,存入localStorage
。
相关代码:custom-display.vue的restore()
取出部分同 2。
五、代码仓库
https://gitee.com/wy_eric/drag-table
0 条评论