基于element-ui实现表格组件之动态表头

4,198 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

前言

大家好,最近我在做一个PC端基于element-ui封装一个上层的表格的组件,组件涵盖了表格的大部分相关功能,比如: 基础表格、分页、查询、动态表头等,功能的话,我是边构思,边写代码,然后在把思路和代码写成文章发到掘金的。 对这个组件有兴趣的,可以从我之前的第一篇写此组件的文章开始看。 这篇文章主要是写动态表头的实现. 主要的效果如下:

987.gif 因为表格支持多表头的功能,所以我们需要对动态表头做如下功能:

  • 实现显示隐藏一级表头、二级表头、三级表头
  • 实现表头查询功能
  • 实现表头拖拽排序功能 以上是需要实现的功能,在明确需求后,会发现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)
},

那么,至此基础的功能就实现了。 里边的一些工具方法,比如setCachegetCache其实只是做了简单的一层封装

// 设置缓存数据
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
  }
}

过滤

实现过滤方法就很简单了。 输入框输入数据后,监听到数据变化, 然后调用treefilter方法传入输入的值.然后通过树组件提供了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-dropallow-drag. 其中

  • allowDrag 表示哪些节点可以拖动
  • allowDrop 表示可以放置在哪个节点前面或者后面
// 拖拽方法
allowDrop(draggingNode, dropNode, type) {
  // 只能同级拖拽排序,不能拖到同级的内部去
  if (draggingNode.level === dropNode.level && type !== 'inner') {
    return true
  }
},
// 设置每个节点都可以拖拽
allowDrag() {
  return true
},

ok, 完成了。以下看一下拖拽和排序的效果

gggg.gif

写在最后

那么,基本上表格及表格周边的功能都覆盖到了。 以后在项目中碰到一些比较通用的功能,还会继续加进来。 更好的完善这个组件. 让它能更好用! 如果对这个表格组件感兴趣的话,可以点这里我已经把它开源出来了。 欢迎Star, 也欢迎大家对本文章点赞、关注一波~ 路漫漫,大家一起进步!