el-table封装,增加列拖动顺序及显隐,各位杠精你们要的简单版

1,599 阅读2分钟

背景

写了篇文章不要再用json去封装element的表格了,用函数式组件去劫持它吧,文章里写了两个版本的代码,只是想记录一下自己的一些思路和封装历程,写完之后被喷复杂,那就给你们个精简版吧,再问问你们复杂在哪里,或者有没有更好的解决方案

回应为啥不用json配置

首先,本次整改的项目设计到需要整改的表格有100+个,全部去转json意味着大量的工作量,已经存在的风险性提高,而本方案只需要修改标签名,其余的不变。

其次为啥要使用json?难道你在template中写的属性在json中少了?难道你用for循环你的数组不用去构建吗?

再者,你的封装别人接手的时候是否要去读你的api才能使用?

还有,嵌套表头可以去试试,看json是否能做

实现效果

image.png

调用

和原本的el-table一样,只是换了个标签变成成自己的组件

image.png

原理

操作组件里的$vnode,将vnode下的子节点传给表格组件,使用render函数去渲染node数组。全部就以下两个核心方法。剩余的全是操作缓存数组的方法,你也可以不止我实现的这些功能,自己去增加缓存数组的属性就行。

一个获取缓存在localStorage里的缓存数据,并根据最新的node节点做刷新

initStorage() {
      this.storageList = []
      const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
      // 不管是否初次还是要做一下处理,万一页面有修改,做一下更新,以最新的node节点数组为准
      let list = storage[this.storageName] ? storage[this.storageName] : []
      this.$vnode.componentOptions.children.forEach(node => {
        // 以label为准,因为可能会改文本
        if (!(!node.componentOptions || node.componentOptions.propsData.type) && list.findIndex(i => i.label === node.componentOptions.propsData.label) < 0) {
          // 非插槽且 不是特殊类型的 找不到就加上
          const propsData = JSON.parse(JSON.stringify(node.componentOptions.propsData))
          if (propsData.fixed === undefined || propsData.fixed === false) {
            propsData.fixed = false
          } else {
            propsData.fixed = propsData.fixed ? propsData.fixed : 'left'
          }
          list.push({
            fixed: false, // 默认新增的都是不固定
            show: true, // 默认新增的都是显示的
            ...propsData
          })
        }
      })
      // 必须在节点数组存在的才有意义
      list = list.filter(item => this.$vnode.componentOptions.children.find(n => {
        return n.componentOptions && item.label === n.componentOptions.propsData.label
      }))
      this.storageList = list
    },

一个根据当前的配置数据进行表格渲染

// 根据缓存的数组进行渲染表格
    updateTable() {
      // 特殊类型
      const childrenNodes = this.$vnode.componentOptions.children.filter(node => node.componentOptions && node.componentOptions.propsData.type)
      this.storageList.forEach(item => {
        if (item.show) {
          const node = this.$vnode.componentOptions.children.find(n => n.componentOptions && 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
      // 通过key值触发表格刷新,避免拖拽由于数组长度一致导致diff算法无法识别
      this.config.key = Math.random() + ''
      // 控制下表格重新刷新
      // this.getInstance()?.doLayout()
      const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
      storage[this.storageName] = this.storageList
      window.localStorage.setItem('tableStorage', JSON.stringify(storage))
    }

image.png 如果以上两个方法加上注释不到五十行都让你觉得无比复杂,无法阅读和维护的话,那下文代码就可以不用看了

HfTable组件代码

<template>
  <div class="hf-table">
    <el-popover
      placement="bottom-end"
      popper-class="table-cloumn-setting-popper"
      trigger="click"
    >
      <div class="setting-row-content">
        <div style="text-align:right">
          <el-button @click="delAllStorage">恢复系统表格设置</el-button>
          <el-button @click="delStorage">恢复当前表格设置</el-button>
        </div>
        <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>
    <!-- 按钮容器 -->
    <div
      class="table-operate-btn-content"
    >
      <!-- 插槽自定义表格上方操作栏 -->
      <slot name="operateBtnContent">
        <!-- 默认左右都有操作按钮,如果单纯想左或者想右,请在插入具名插槽 -->
        <div class="operate-btn-content">
          <!-- 流式左右布局 -->
          <slot name="btnContentLeft">
            <div />
          </slot>
          <slot name="btnContentRight">
            <div />
          </slot>
        </div>
      </slot>
    </div>
    <div :style="{height:`${tableHeight}px`}">
      <new-table ref="table" :config="config" />
    </div>
  </div>
</template>
<script>
import draggable from 'vuedraggable'
import newTable from './myTable.jsx'
import setHeight from '@/mixins/setHeight'
const components = { newTable, draggable }
export default {
  name: 'HfTable',
  components,
  mixins: [setHeight],
  props: {
    storageName: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      showTable: false,
      storageList: [],
      name: '',
      config: {
        children: [],
        attrs: {},
        listeners: {},
        key: ''
      }
    }
  },
  watch: {
    '$attrs': {
      handler(newV) {
        this.$set(this.config, 'attrs', newV)
      },
      deep: true,
      immediate: true
    }
  },
  mounted() {
    this.initStorage()
    this.updateTable()
  },
  methods: {
    // 动态列渲染表格时,需要在列渲染完成后重新渲染一下表格
    reload() {
      this.$nextTick(() => {
        this.initStorage()
        this.updateTable()
      })
    },
    getInstance() {
      // const ref = this.$children.find(i => i.$options._componentTag === 'el-table')
      // return ref
      return this.$refs.table.$refs?.elTable
    },
    delStorage() {
      this.$confirm('恢复当前表格设置将清除当前表格设置并刷新页面是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
        storage[this.storageName] = []
        window.localStorage.setItem('tableStorage', JSON.stringify(storage))
        location.reload()
      })
    },
    delAllStorage() {
      this.$confirm('恢复系统表格设置将清除当前表格设置并刷新页面是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        window.localStorage.removeItem('tableStorage')
        location.reload()
      })
    },
    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('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
      // 不管是否初次还是要做一下处理,万一页面有修改,做一下更新,以最新的node节点数组为准
      let list = storage[this.storageName] ? storage[this.storageName] : []
      this.$vnode.componentOptions.children.forEach(node => {
        // 以label为准,因为可能会改文本
        if (!(!node.componentOptions || node.componentOptions.propsData.type) && list.findIndex(i => i.label === node.componentOptions.propsData.label) < 0) {
          // 非插槽且 不是特殊类型的 找不到就加上
          const propsData = JSON.parse(JSON.stringify(node.componentOptions.propsData))
          if (propsData.fixed === undefined || propsData.fixed === false) {
            propsData.fixed = false
          } else {
            propsData.fixed = propsData.fixed ? propsData.fixed : 'left'
          }
          list.push({
            fixed: false, // 默认新增的都是不固定
            show: true, // 默认新增的都是显示的
            ...propsData
          })
        }
      })
      // 必须在节点数组存在的才有意义
      list = list.filter(item => this.$vnode.componentOptions.children.find(n => {
        return n.componentOptions && item.label === n.componentOptions.propsData.label
      }))
      this.storageList = list
    },
    // 根据缓存的数组进行渲染表格
    updateTable() {
      // 特殊类型
      const childrenNodes = this.$vnode.componentOptions.children.filter(node => node.componentOptions && node.componentOptions.propsData.type)
      this.storageList.forEach(item => {
        if (item.show) {
          const node = this.$vnode.componentOptions.children.find(n => n.componentOptions && 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
      // 通过key值触发表格刷新,避免拖拽由于数组长度一致导致diff算法无法识别
      this.config.key = Math.random() + ''
      // 控制下表格重新刷新
      // this.getInstance()?.doLayout()
      const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {}
      storage[this.storageName] = this.storageList
      window.localStorage.setItem('tableStorage', JSON.stringify(storage))
    }
  }
}
</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: 7px;
      top: 5px;
      cursor: pointer;
    }
    .table-operate-btn-content{
      width: calc(100% - 40px);
      min-height: 28px; // 给一个最低的高度,防止没有插槽,右侧icon被吞
     .operate-btn-content {
        display: flex;
        justify-content: space-between;
        align-items: center;
        min-height: 28px;
        padding-bottom: 6px;
      }
    }
  }
</style>


table.jsx代码

export default {
  name: 'MyTable',
  props: {
    config: {
      type: Object,
      default: () => ({})
    }
  },
  render(h) {
    const scopedSlots = {}
    Object.keys(this.$parent.$scopedSlots).forEach(key => {
      if (key !== 'default') {
        scopedSlots[key] = this.$parent.$scopedSlots[key]
      }
    })
    return h('el-table', {
      ref: 'elTable',
      key: this.config.key || '',
      attrs: { ...this.config.attrs },
      on: { ...this.config.listeners },
      scopedSlots
    }, this.config.children)
  }
}

结语

我也不是要杠什么,也不是一定说自己的方案最好最牛逼,我也改了很多版,从设计到应用,到问题出现到重构,从一开始用函数式组件,到后面用jsx,都是一个探索的过程,但是烦的是那种看都不看就在说复杂说无法维护的,能不能给个切合实际的设计思路出来?是真特么口嗨不用成本