我这样封装Element UI表格,你还说不优雅?

363 阅读1分钟

在我们实际开发过程中UI组件的二次封装是一种基操。可以节省代码的臃余。让整个页面看上去更加整洁,增强代码的可维护性。

背景

公司是做传统业务开发的,后台管理系统中百分之80的页面都是表格数据展示,为了便于后期维护,便考虑将el-table进行封装,决心做一款灵活,简单,便维护的表格组件。

功能如下:

  • 表格自带分页
  • 表格自动后台请求数据
  • 支持多选、单选
  • 兼容el-table一切方法与属性

基础用法

<template>
   <zt-table :tableOption="tableOption"
              ref="ztTable">
      <el-table-column label="操作"
                       width="160">
        <template slot-scope="scope">
          <el-button type="text">编辑</el-button>
          <el-button type="text">删除</el-button>
          <el-button type="text">查看</el-button>          
        </template>
      </el-table-column>
    </zt-table>
 </template>
 <script>
 import ztTable from '@/components/ztTable.vue' //这里可以挂在全局
 
 export default {
      components: { ztTable },
      data() {
          return {
           /* -----------表格配置------------*/
              tableOption: {
                api: {
                  url: '/hisEmergencyResponse/responsePage', //列表api配置
                  type: 'post', //列表api请求类型
                  params: { //params里面的属性,发请求的时候会携带上用于数据的条件筛选
                       name:'' //名称搜索
                       phone:'' //电话号码
                  }, 
                },
                option: [
                  //表格参数配置
                  this.T('姓名', 'source'),
                  this.T('电话', 'eventTypeCenter'),
                  this.T('岗位', 'happenLocation'),
                  this.T('部门', 'happenTime'),
                  this.T('职责', 'responseLevel')
                ],
              },
          }
      },
      methods:{
          //该方法可以单独抽离到一个公共的工具JS文件,或者 minxin 里面
          T(label, prop, tag, width, fixed, sortable){  //这些属性的配置可参照el-table
               return {
                label, //表头
                prop,  //值
                tag,   //插槽
                width, //单元格宽度
                fixed, //是否固定单元格
                sortable, //是否可以排序
              };
          }
      }
 }
 </script>
 

扩展方法

插槽

场景:比如表格里面有一些字段是字典数据返回0或1,需要根据字典匹配中文

<template>
 <zt-table :tableOption="tableOption"
            ref="ztTable">
       <template #sex='{data}'>
           {{data.sex == 0? '男':'女'}}
       </template>
  </zt-table>
</template>
<script>
import ztTable from '@/components/ztTable.vue' //这里可以挂在全局

export default {
    components: { ztTable },
    data() {
        return {
         /* -----------表格配置------------*/
            tableOption: {
              api: {
                url: '/hisEmergencyResponse/responsePage', //列表api配置
                type: 'post', //列表api请求类型
                params: { //params里面的属性,发请求的时候会携带上用于数据的条件筛选
                     name:'' //名称搜索
                     phone:'' //电话号码
                }, 
              },
              option: [
                //表格参数配置
                this.T('性别', 'sex','sex'),
              ],
            },
        }
    },
   
}
</script>

渲染,分页,单选、多选回调

场景:比如表格数据加载完成,需要做某件事!表格点击分页,需要做某件事!单选多选功能等等业务需求

 <template>
   <zt-table :tableOption="tableOption"
              ref="ztTable"
              :isSelect='true' //开启多选默认关闭false
              :isRadio='true' //开启单选默认关闭false
              @onsuccess='onsuccess'  //渲染数据成功的回调
              @handleCurrentChange='handleCurrentChange' //分页的回调
              @handleRadioChange='handleRadioChange' //表格单选
              @handleSelectionChange='handleSelectionChange' //表格多选
              >
    </zt-table>
 </template>
 
  <script>
 import ztTable from '@/components/ztTable.vue' //这里可以挂在全局
 
 export default {
      components: { ztTable },
      methods:{
          onsuccess(data){
              //data是列表数据
          },
          handleCurrentChange(page){
               //page当前页面
          },
          handleRadioChange(data){
              //当前选中的数据集合
          },
          handleSelectionChange(e){
          
          }
      }
     
 }
 </script>
渲染拦截

场景:比如表格数据字段需要修改,就必须在渲染之前做一次拦截,修改完成再次渲染

 <template>
   <zt-table :tableOption="tableOption"
              ref="ztTable">
    </zt-table>
 </template>
 
 <script>
     import ztTable from '@/components/ztTable.vue' //这里可以挂在全局
 
     export default {
          components: { ztTable },
          data() {
              return {
               /* -----------表格配置------------*/
                  tableOption: {
                    api: {
                      url: '/hisEmergencyResponse/responsePage', //列表api配置
                      type: 'post', //列表api请求类型
                      params: {},
                      callback(data) {  //拦截器函数
                          data.forEach(item=>{
                              item.userName = item.name  //修改一下数据字段
                          })
                          return data //这里要记得写return 不然数据没法渲染
                      }
                    },
                    option: [
                      //表格参数配置
                      this.T('性别', 'sex','sex'),
                    ],
                  },
              }
          },
     
     }
 </script>
手动刷新表格数据

场景:有时候不想表格自己发请求加载,需要自己手动加载

<template>
  <zt-table :tableOption="tableOption"
             ref="ztTable" 
             :isLoadData='false' //该属性默认开启true
   >
   </zt-table>
</template>

<script>
    import ztTable from '@/components/ztTable.vue' //这里可以挂在全局

    export default {
         components: { ztTable },
         methods:{
             onTable(){
                 this.$refs.ztTable.reload() //调用这个方法,能重新加载数据
             }
         }
    
    }
</script>

讲到这里组件的大致用法就讲完了。接下来把源码分享给大家。有需求的同学可以copy到自己的项目里面试一下哦。

注意:源码里很多功能,是与当前项目需求产生耦合的。并非开箱即用。需伙伴们自行删减!这里只是给伙伴们一种思路与参照

<template>
  <div>
    <el-table :data="tableData"
              :summary-method="getSummaries"
              v-bind="$attrs"
              :row-key="getRowKey"
              v-on="$listeners"
              ref="table"
              stripe
              border
              :header-cell-style="isHeadColor ? headStyle : {}"
              :empty-text="emptyText"
              :highlight-current-row="true"
              @selection-change="handleSelectionChange"
              @row-click="handleRadioChange"
              :height="height ? height=='auto'?'':height : tableHeight">
      <el-table-column type="selection"
                       width="55"
                       :reserve-selection="true"
                       v-if="isSelect" />
      <el-table-column align="center"
                       v-if="isRadio"
                       width="50"
                       label="选择">
        <template slot-scope="scope">
          <el-radio :label="scope.row.id"
                    v-model="rowRadio"></el-radio>
        </template>
      </el-table-column>
     
      <el-table-column type="index"
                       v-if="isOrder"
                       label="序号"
                       width="60"
                       align="center">
        <template slot-scope="scope">
          <span v-text="getIndexNum(scope.$index)"></span>
        </template>
      </el-table-column>
      <el-table-column v-for="(item, index) in tableOption.option"
                       :key="index"
                       :prop="item.prop"
                       :label="item.label"
                       :width="item.width ? item.width : null"
                       :sortable="item.sortable ? item.sortable : false"
                       :show-overflow-tooltip="item.showTip || false"
                       :align="item.align || 'left'"
                       :fixed="item.fixed || false"
                       :header-align="item.headerAlign || 'left'">
        <template v-if="item.children && item.children.length">
          <el-table-column v-for="(childrenItem, childrenIndex) in item.children"
                           :key="childrenIndex"
                           :prop="childrenItem.prop"
                           :label="childrenItem.label"
                           :width="childrenItem.width ? childrenItem.width : null"
                           :sortable="childrenItem.sortable ? childrenItem.sortable : false"
                           :align="childrenItem.headerAlign || 'left'"
                           :fixed="childrenItem.fixed || false"
                           :header-align="childrenItem.headerAlign || 'left'">
            <template slot-scope="scope">
              <template v-if="childrenItem.tag">
                <slot :name="childrenItem.tag"
                      :data="scope.row"></slot>
              </template>
              <span v-else>{{ scope.row[childrenItem.prop] }}</span>
            </template>
          </el-table-column>
        </template>

        <template slot-scope="scope">
          <template v-if="item.tag">
            <slot :name="item.tag"
                  :data="scope.row"></slot>
          </template>
          <span v-else>{{ scope.row[item.prop] }}</span>
        </template>
      </el-table-column>

      <slot />
    </el-table>

    <el-pagination background
                   v-if="isPage"
                   small
                   class="table_pagination"
                   :current-page.sync="page.pageNo"
                   :page-sizes="page.list"
                   :page-size="page.pageSize"
                   :layout="page.layout"
                   :total="page.total"
                   @current-change="handleCurrentChange"
                   @size-change="handleSizeChange" />
  </div>
</template>

<script>
export default {
  props: {
    emptyText: {
      type: String,
      default: '暂无数据',
    },
    tableOption: {
      type: Object,
    },
    // 选中数据
    selectData: {
      type: Array,
      default: () => {
        return []
      },
    },
    height: {
      type: String,
    },
    isOrder: {
      //是否开启序号
      type: Boolean,
      default: true,
    },
    isRadio: {
      //是否选择
      type: Boolean,
      default: false,
    },
    isSelect: {
      //是否选择
      type: Boolean,
      default: false,
    },
    RowKey: {
      //选中的字段名
      type: String,
      default: 'id',
    },
    isLoadData: {
      //是否一开始就加载
      type: Boolean,
      default: true,
    },
    isPage: {
      //是否开启分页
      type: Boolean,
      default: true,
    },
    isHeadColor: {
      //是否开启表头部样式
      type: Boolean,
      default: false,
    },
  },

  data() {
    return {
      rowRadio: '',
      headStyle: { color: '#909399', background: '#fafafa' },
      tableData: [],
      tableHeight: 0,
      page: {
        list: [5, 10, 15, 20, 25, 50, 100, 200, 400],
        total: 0,
        pageSize: 10,
        pageNo: 1,
        layout: 'total,sizes,prev,pager,next,jumper',
      },
      isShow: true,
    }
  },
  updated() {
    this.doLayout()
  },
  destroyed() {
    window.removeEventListener('resize', this.getTableHeight)
    window.removeEventListener('resize', this.doLayout)
  },
  created() {
    if (this.isLoadData) {
      this.initTable()
    }

    window.addEventListener('resize', this.getTableHeight)
    window.addEventListener('resize', this.doLayout)
  },
  mounted() {
    this.$nextTick(() => {
      this.getTableHeight()
      this.doLayout()
    })
  },
  methods: {
    getRowKey(row) {
      return row[this.RowKey]
    },
    toogleExpand(row) {
      let $table = this.$refs.table
      this.tableData.map((item) => {
        if (row.id != item.id) {
          $table.toggleRowExpansion(item, false)
        }
      })
      $table.toggleRowExpansion(row)
    },

    doLayout() {
      setTimeout(() => {
        if (this.$refs['table']) {
          this.$refs['table'].doLayout()
        }
      }, 1000)
    },
    getDataItems(dataItems, data) {
      dataItems.push(data)
      if (data.list && data.list.length > 0) {
        data.list.forEach((d) => {
          this.getDataItems(dataItems, d)
        })
      }
      return dataItems
    },
    getSummaries(param) {
      const { columns, data } = param
      const sums = []
      columns.forEach((column, index) => {
        if (index === 0) {
          sums[index] = '合计'
          return
        }
        const values = data.map((item) => Number(item[column.property]))
        if (!values.every((value) => isNaN(value))) {
          sums[index] = values.reduce((prev, curr) => {
            const value = Number(curr)
            if (!isNaN(value)) {
              return prev + curr
            } else {
              return prev
            }
          }, 0)
        }
      })
      return sums
    },
    /* 表格序号计算 */
    getIndexNum($index) {
      return (this.page.pageNo - 1) * this.page.pageSize + $index + 1
    },
    // 重新加载数据(重置到第一页)
    reload() {
      if (this.page.pageNo !== 1) {
        this.page.pageNo = 1
      }
      this.isShow = false
      this.initTable()
    },

    async initTable() {
      let { url, type, params } = this.tableOption.api
      const new_params = Object.assign(
        {
          current: this.page.pageNo,
          size: this.page.pageSize,
          page: this.page.pageNo,
        },
        params
      ) // 右值覆盖左值,返回左值

      try {
        let { data } = await this.getData(url, type || 'post', new_params)

        if (typeof data == 'string') {
          data = JSON.parse(data)
          data.records = data.list || data.content
        }
        this.tableData = data.records || data.tableData || data
        this.doLayout()
        this.setRowSelection(this.selectData) //设置选中数据
        // this.tableData = createTree(this.tableData);
        if (this.tableOption.api.callback) {
          this.tableData = this.tableOption.api.callback(this.tableData)
        }
        this.page.total = Number(data.total || data.totalElements || 0)
        this.tableOption.callback && this.tableOption.callback(data)

        if (this.isShow) {
          this.$emit('onsuccess', this.tableData)
        }

        console.log('表格数据', this.tableData)
      } catch (err) {
        console.log('报错了', err)
      }
    },

    getData(url, type, params) {
      return new Promise(function (resolve, reject) {
        Object.toAjaxJson(url, null, params, true, type, function (res) {
          resolve(res)
        })
      })
    },

    handleCurrentChange(currentPage) {
      this.page.pageNo = currentPage
      this.$emit('handleCurrentChange', currentPage)
      this.initTable()
    },

    handleSizeChange(size) {
      this.page.pageSize = size
      this.page.pageNo = 1
      this.$emit('handleSizeChange', size)
      this.initTable()
    },

    handleRadioChange(row, index) {
      this.rowRadio = row.id
      this.$emit('onRadio', row)
    },
    handleSelectionChange(e) {
      this.$emit('onTab', e)
    },

    isLastPage() {
      // 根据总的数据条数total和一页显示的数据条数,得到总页数
      let lastPage = Math.ceil(this.page.total / this.page.pageSize)
      // 判断当前页是否是最后一页
      if (this.page.pageNo === lastPage) {
        if (this.page.pageNo <= 1) {
          this.page.pageNo = 1
          return
        }
        this.page.current--
      }
    },
    //设置选中数据
    setRowSelection(selectData) {
      if (selectData && selectData.length > 0) {
        this.$nextTick(() => {
          this.tableData.forEach((k1) => {
            k1.isSelect = 0
            selectData.forEach((k2) => {
              if (k2.id === k1.id) {
                k1.isSelect = 1
                this.$refs.table.toggleRowSelection(k1, true)
              }
            })
          })
        })
      }
    },

    selectable(rows) {
      if (rows.isSelect == 1) {
        return false
      } else {
        return true
      }
    },

    getTableHeight() {
      let parent = this.$parent
      let seachHeight = 0
      while (parent.$options._componentTag != 'ztPage') {
        parent = parent['$parent']
      }
      parent.$children.forEach((el) => {
        if (el.$options._componentTag == 'zt-search') {
          seachHeight = el.$el.clientHeight
        }
      })
      this.tableHeight = window.innerHeight - seachHeight - 200
    },
  },
}
</script>

<style lang="scss" scoped>
.table_pagination {
  margin-top: 10px;
  width: 100%;
  display: flex;
  justify-content: center;
}
::v-deep .el-radio__label {
  display: none;
}
::v-deep .el-table__expand-icon {
  display: none;
}
</style>