功能列表

  1. 鼠标在表头文字区域按压、拖拽,调整列序;
  2. 根据个人需要自定义各列的显隐;
  3. 一键恢复初始列序、列宽、显隐;
  4. 本地保存列序、列宽、显隐等记忆数据,不受刷新影响;
  5. 系统内各表格的记忆数据互不干扰,和谐共存。

体验demo:https://app.wy310.cn/drag-table/#/

一、自定义列序

1. 如何选型?

Element对el-table做了严谨的封装,如果以操作dom的方式来改变列序可能会出现很多意想不到的问题(这类方法可能会用到sortable.jsvuedragable库)。而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是不需要怎么改动的,这里我还不敢说完全不用改,是因为:

  1. 我们还需要确保所有字段都具备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里,我们可以得到一些有用的数据:

  1. componentOptions.propsData(列属性、表头渲染器)
  2. data.scopedSlots.default(单元格渲染器)
this.$slots.default下的componentOptions.propsData
this.$slots.default下的data.scopedSlots.default
visibleColumn数据构成

列属性v-bind="col"绑定在各列上,提供了各列需要的labelpropsortablewidthfixed等属性;

表头渲染器dd-table-column中被赋值给columnConfig.renderHeader,实现了自定义表头的渲染。

单元格渲染器dd-table-column中被赋值给columnConfig.renderCell,实现了自定义内容的渲染。

5. 如何实现拖拽改变列序?

在各列的表头中的名为header的插槽中创建一个div.thead-cell,监听其上的mousedownmousemovemouseup事件,mousedown时记录当前列的索引——startmouseup时记录最终悬浮列的索引——end,根据startend这两个索引,经过易位计算,最后得出最新的数组,通过vue的双向绑定,即完成了表格列序的调整。

.thead-cell结构
鼠标按压抬起动画

.thead-cell上绑定了鼠标事件,鼠标按下后,.virtual被赋予各列的宽度,它是透明的,是为了显示左、右虚线border而存在的,这样能给用户传达一个视觉信号——你将要把所选列落在这里。

为了实现拖拽的交互效果,需要在el-table标签上征用两个属性:

  1. header-cell-class-name
    绑定一个函数,动态给表头单元格添加 class,从而实现拖动中的虚线效果。
  2. cell-class-name
    绑定一个函数,动态给表体单元格添加 class,用于区分选中列。
6. 表格数据JSON化还会带来哪些问题?

第3步只说了如何获取renderCellrenderHeader,但如何渲染还未解决:

  1. 单元格如何渲染自定义的内容?
  2. 表头如何渲染自定义的内容?
<!-- 单元格内插入一个按钮 -->
<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,重写renderCellrenderHeader函数改变其返回内容。

<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,当拖动表头改变了列的宽度的时候会触发该事件,参数为newWidtholdWidthcolumnevent

我们只需要在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组件提供了一个名为addressprop,用于区分不同位置的表格,比如<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.25.26.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

4+
分类: vue
浏览:4,245

0 条评论

发表评论

电子邮件地址不会被公开。

你必须允许浏览器启用JavaScript才能看到验证码

Scroll Up