封装Element-UI的Table

在开发后台管理系统时,一个常见的页面布局是:上面一堆过滤搜索条件,下面一个表格显示搜索结果。

而这个表格通常要满足几点需求:

  1. 表头可以显示图标
  2. 带复选框
  3. 每行行尾有操作按钮如删除、修改
  4. 分页

在之前的项目里,我直接用Element-UI提供的组件,开发起来比较繁琐。尤其是团队其他人参与进来,每个人实现的风格可能不大一致,后期维护起来也比较痛苦。

而且新的项目里多了这样一个需求:用户可以配置显示哪些列。也就是说列不是写死的,是动态的。原来的模式不能满足。

因此在新的项目里我对表格功能进行了封装,这样在使用表格时配置一下Column和数据来源就可以了。而且团队其他人用的风格也一致了。

下面记录一下我是如何封装的。

组件

main.vue

<template>
  <div>
    <!--
      父组件没有在$props中声明的属性和方法会通过$attrs$listeners直接传递下去这两个属性在封装透明的组件时特别有用
      这里的重点是把外面传进来的column数组通过自定义的ElColumn组件渲染成实际的el-table-column
    -->
    <el-table
      :key="key"
      ref="elTable"
      v-loading="$attrs.loading"
      v-bind="$attrs"
      v-on="$listeners"
      :data="data">
      <ElColumn v-for="(col, index) in column" :key="index" :column="col"></ElColumn>
    </el-table>
    <!--
      由于$attrs给上面用了这里用一个pagination的object来接收属性避免冲突
    -->
    <el-pagination
      v-if="pagination"
      v-bind="pagination"
      v-on="$listeners"
      @current-change="handleCurrentPageChange"
      @size-change="handleSizeChange"
      :current-page.sync="currentPage"
      :page-size.sync="pageSize"
      :total="total"
      :layout="pagination.layout || 'total, prev, pager, next, jumper'"
      :style="{'margin-top': pagination.top || '3px', 'text-align': pagination.align || 'center'}">
    </el-pagination>
  </div>
</template>

<script>
import ElColumn from './column.vue'
export default {
  name: 'ElTableWrap',
  components: {
    ElColumn
  },
  props: {
    column: Array,
    data: Array,
    getData: Function,
    isGetDataOnCreate: { type: Boolean, default: true },
    pagination: { type: Object, default: null }
  },
  data() {
    return {
      key: 1,
      currentPage: 1,
      pageSize: (this.pagination && this.pagination.pageSize) || 10,
      total: 0
    }
  },
  created() {
    if (this.isGetDataOnCreate && this.getData) this.getTableData()
  },
  methods: {
    // 有时候父节点宽度改变时,表格没有自适应,重新渲染一下
    reRenderTable() {
      this.key += 1
      this.$refs.elTable.doLayout()
    },
    // 查询后台获取某一页的数据
    getTableData(page_num, page_size) {
      this.currentPage = page_num || 1
      if (page_size) this.pageSize = page_size
      this.$emit('update:loading', true)
      this.getData(this.currentPage, this.pageSize).then(res => {
        this.$emit('update:loading', false)
        if (res.status) {
          this.total = res.total
          this.$emit('update:data', res.data)
        }
      })
    },
    refreshCurrentPage() {
      this.getTableData(this.currentPage)
    },
    handleCurrentPageChange(page_num) {
      this.getTableData(page_num)
    },
    handleSizeChange(page_size) {
      this.getTableData(1, page_size)
    }
  }
}
</script>

column.vue

<template>
  <!--
    这个组件主要是把传进来的column渲染成真正需要的el-table-column
    第一种情况是column包含了render函数需要用expand-dom这个函数式组件把render函数渲染出来
  -->
  <el-table-column
    v-if="column.render || column.renderHeader"
    v-bind="column">
    <template slot="header" slot-scope="scope">
      <expand-dom
        v-if="column.renderHeader"
        :scope="scope"
        :render="column.renderHeader">
      </expand-dom>
      <span v-else>{{ scope.column.label }}</span>
    </template>

    <template slot-scope="scope">
      <expand-dom
        v-if="column.render"
        :scope="scope"
        :render="column.render">
      </expand-dom>
      <span v-else>{{ scope.row[scope.column.property] }}</span>
    </template>

    <!-- 这里渲染树形数据 -->
    <template v-if="column.children">
      <ElColumn v-for="(col, index) in column.children" :key="index" :column="col"></ElColumn>
    </template>
  </el-table-column>

  <!-- 另一种情况column里面没有包含render函数 -->
  <el-table-column
    v-else
    v-bind="column">
    <template v-if="column.children">
      <ElColumn v-for="(col, index) in column.children" :key="index" :column="col"></ElColumn>
    </template>
  </el-table-column>
</template>

<script>
export default {
  name: 'ElColumn',
  components: {
    // 这里定义了一个functonal component,把column里面的render函数渲染出来
    expandDom: {
      functional: true,
      props: {
        scope: Object,
        render: Function
      },
      render(h, context) {
        return context.props.render(h, context.props.scope)
      }
    }
  },
  props: {
    column: Object
  }
}
</script>

用法

<template>
  <ElTableWrap
    ref="table"
    size="mini"
    :loading.sync="isLoadingTableData"
    :column="column"
    :data.sync="tableData"
    :get-data="getTableData"
    :pagination="pagination"
    highlight-current-row
    @select="onSelect"
    @select-all="onSelect">
  </ElTableWrap>
</template>
<script>
import getData from 'somewhere'
import ElTableWrap from './main.vue'
export default {
  name: 'Test',
  components: {
    ElTableWrap
  },
  data() {
    return {
      isLoadingTableData: false,
      tableData: [],
      tableColumn: [
        {type: 'selection', width: 40},
        {prop: 'foo', label: 'Foo', width: 70, fixed: true},
        {prop: 'bar', label: 'Bar', width: 70, formatter: this.formatBar},
        {width: 100, renderHeader: this.renderHeader, render: this.render, fixed: 'right'}
      ],
      selection: [],
      pagination: {
        pageSize: 20,
        top: '5px'
      }
    }
  },
  methods: {
    getTableData(page_num, page_size) {
      return getData({page_num, page_size})
    },
    refreshTable() {
      this.$refs.table.getTableData()
    },
    refreshCurrentPage() {
      this.$refs.table.refreshCurrentPage()
    },
    onSelect(selection, row) {
      this.selection = selection
    },
    formatBar(row, column, cellValue, index) {
      return cellValue
    },
    renderHeader(h, scope) {
      return h('i',
        {
          class: 'el-icon-menu',
          style: 'font-size: 18px; cursor: pointer;',
          on: { click: () => { console.log('header button clicked') } }
        }
      )
    },
    render(h, scope) {
      return h('el-button',
        {
          on: { click: () => { console.log('clicked button on row: ', scope.row)} }
        },
        'action'
      )
    }
  }
}
</script>

可以看到,只要把data里面的tableColumn配置好,引进获取数据的接口getData,就可以了。因为这个封装相对于里面的el-table差不多是透明的,更多用法只要查阅el-table的api,相应地设置props属性、tableColumn和监听事件即可。而分页相关的功能修改data里面的pagination对象即可。

最后render函数也可以使用JSX替代手写。

总结

经过了简单的二次封装,我们可以通过配置数组生成需要的表格。满足了可以动态配置列的需求。也提升了团队的开发效率。