基于 Vxe-Table 的二次封装:提升开发效率与可维护性(更新版本)

3,422 阅读6分钟

在现代前端开发中,表格组件是数据展示的核心工具之一。Vxe-Table 是一款功能强大、性能优异的 Vue 表格组件,但在实际项目中,直接使用 Vxe-Table 可能会导致代码重复、维护困难等问题。因此,对 Vxe-Table 进行二次封装,可以显著提升开发效率和代码的可维护性。本文将分享如何对 Vxe-Table 进行二次封装,并提供一个实用的封装示例。


为什么要二次封装 Vxe-Table?

  1. 减少重复代码
    项目中可能有多个表格,它们的配置项(如分页、排序、列定义)往往相似。通过封装,可以将通用逻辑提取出来,减少重复代码。
  2. 统一风格与行为
    封装后可以统一表格的样式、分页逻辑、排序规则等,确保项目中的表格风格一致。
  3. 增强可维护性
    将表格逻辑集中管理,后续修改或扩展时只需调整封装组件,而不需要修改每个使用表格的地方。
  4. 提升开发效率
    封装后,开发者只需关注业务逻辑,无需重复配置表格的基础功能。

二次封装的核心思路

  1. 提取通用配置
    将分页、排序、列定义等通用配置提取到封装组件中。
  2. 暴露必要的 Props 和 Events
    通过 Props 接收外部数据(如表格数据、列配置),通过 Events 暴露内部事件(如分页变化、排序变化)。
  3. 支持自定义插槽
    提供插槽支持,允许外部自定义表格内容(如表头、操作列)。
  4. 集成常用功能
    封装分页、加载状态、空数据提示等常用功能。

以上来源于deepseek解答, 解答就仁者见仁 , 智者见智了


封装代码

以下代码是经过优化后的代码,

将我司项目所使用到的所有表格功能都封装进去了,

大概封装后功能的有

  1. 表格查询表单封装,统一样式
  2. toolbar工具栏插槽
  3. 前端自己处理分页,且兼容筛选,排序等
  4. 前端处理导出数据, 也可以通过接口导出
  5. vxe-table本身自带的渲染器功能
  6. vxe-table本身的其他功能, 例如单元格编辑等

几年前,写过一篇关于vex-table的封装文章了, 在这里: 一个基于vue功能强大的表格组件--vxe-table的二次封装(一)

二次封装示例

组件结构

image.png

以下是新版本基于 Vxe-Table 的二次封装示例:

template代码


  <vxe-grid ref="xTable"
            :key="tableKey"
            :column-config="{ useKey: true }"
            :row-config="rowConfig"
            :auto-resize="true"
            :empty-render="{name: 'NotData'}"
            :data="tableData"
            :nullData="nullData"
            :scroll-y="{ enabled: true}"
            :scroll-x="{ enabled: true}"
            highlight-hover-row
            highlight-current-row
            :align="align"
            :round="true"
            :border="border"
            :stripe="stripe"
            :loading="loading"
            :show-overflow="showOverflow"
            :show-header-overflow="showHeaderOverflow"
            :expand-config="expandConfig"
            :show-header="showHeader"
            :show-footer="showFooter"
            :toolbarConfig="toolbarConfig"
            :sort-config="sortConfig"
            :export-config="exportConfig"
            :edit-config="editConfig"
            :edit-rules="editRules"
            :filter-config="filterConfig"
            :print-config="printConfig"
            :footer-method="footerMethod"
            :size="size"
            :height="height"
            :treeConfig="treeConfig"
            :seq-config="{startIndex: (tablePage.pageIndex - 1) * tablePage.pageSize}"
            :columns="newColumns"
            :radio-config="radioConfig"
            :checkbox-config="checkboxConfig"
            :menu-config="tableMenu"
            @edit-closed="editClosed"
            @edit-acitved="editActived"
            @cell-click="rowClick"
            @cell-dblclick="rowdblClick"
            @menu-click="menuClick"
            @header-cell-click="headerClick"
            @header-cell-menu="headerRightClick"
            @sort-change="onSortChange"
            @checkbox-all="checkboxAll"
            @checkbox-change="checkboxChange"
            >
            
    <!-- 自定义工具栏 -->
    <template #toolbar_buttons>
      <slot></slot>
    </template>
    
    <!-- 自定义顶部查询表单 -->
    <template #form queryform !== null>
      <slot></slot>
    </template>
    
    

    <!-- 分页组件 -->
    <template #pager>
      <vxe-pager v-if="tablePage.showPage"
                 background
                 :layouts="['Sizes', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'FullJump', 'Total']"
                 :current-page.sync="tablePage.pageIndex"
                 :page-size.sync="tablePage.pageSize"
                 :page-sizes="[10,20,50,100,200]"
                 :total="tablePage.total"
                 @page-change="handlePageChange">
      </vxe-pager>
    </template>
  </vxe-grid>
</template>

javascript代码

<script>

import '@/components/vxeTable/renderer.js'
import VXETable from 'vxe-table'
import { exportFile } from '@/util/fileExport' // exportFile 为封装的导出execl的方法
export default {
  name: 'Table',
  props: {
    loading: {
      type: Boolean,
      default: false
    },
    height: {
      type: String || Number,
      default: () => {
        return 'auto'
      }
    },
    stripe: {
      type: Boolean,
      default: true
    },
    border: {
      type: String,
      default: 'none'
    },
    align: {
      type: String,
      default: 'center'
    },
    showOverflow: {
      type: [Boolean, String],
      default: 'tooltip'
    },
    showHeaderOverflow: {
      type: [Boolean, String],
      default: false
    },
    rowConfig: {
      type: Object,
      default: () => {
        return { useKey: true, isHover: true, keyField: 'id' } // id 为 data对象数据中的id
      }
    },
    expandConfig: {
      type: Object,
      default: () => {
        return { accordion: true }
      }
    },
    showHeader: {
      type: Boolean,
      default: true
    },
    showFooter: {
      type: Boolean,
      default: false
    },
    toolbarConfig: {
      type: Object,
      default: () => { }
    },
    size: {
      type: String,
      default: () => { }
    },
    // 工具栏三种形式
    // 1 值为 Function(默认为getTableData)有刷新功能
    // 2 值为'' , 没有刷新功能的工具栏
    // 3 值为 false 没有整体功能栏
    // 4 值为'left’,只有左侧的自定义按钮等工具,没有右侧整体功能
    toolbarFun: {
      type: Boolean | Function | String,
      default: false
    },
    size: {
      type: String,
      default: 'small'
    },
    tablePage: {
      type: Object,
      default: () => {
        return {
          ownPage: false,
          showPage: false,
          pageIndex: 1,
          pageSize: 20,
          total: 0
        }
      }
    },
    queryForm: {
      type: Object | null,
      default: null
    },

    columns: {
      type: Array,
      default: () => {
        return []
      }
    },
    data: {
      type: Array,
      default: () => {
        return []
      }
    },
    nullData: {
      type: Boolean,
      default: true
    },
    treeConfig: {
      type: Object,
      default: undefined
    },
    // 可编辑配置项
    editConfig: {
      type: Object,
      default: () => {
        return {
          enabled: false,
          trigger: 'click',
          mode: 'row',
          beforeEditMethod: true
        }
      }
    },
    // 排序规则配置
    sortConfig: {
      type: Object,
      default: () => {
        return {
           multiple: true
        }
      }
    },
    // editRules: {
      type: Object,
      default: () => {}
    },
    // 复选框配置项
    checkboxConfig: {
      type: Object,
      default: () => {
        return { highlight: true, range: true, trigger: 'default' }
      }
    },
    // 右键快捷菜单配置
    tableMenu: {
      type: Object,
      default: () => {
        return {}
      }
    },
    // 表尾方法
    footerMethod: {
      type: Function,
      default: () => {
        return []
      }
    },
    // 表格操作按钮列
    btns: {
      type: Array,
      default: []
    },
    // 表格操作列宽度控制
    btnsWidth: {
      type: Number,
      default: 250
    },
    // 筛选配置项
    filterConfig: {
      remote: false, // 所有列是否使用服务端筛选,如果设置为 true 则不会对数据进行处理
    },
    // 导出文件
    exportConfigProp: {
      type: Object,
      default: () => {
        return {
          customExtraParams: 'all'
        }
      }
    },
  },

  data() {
    return {
        tableData: [], // 表格自分页后的数据
        btnsColumn: {
            title: '操作',
            minWidth: this.btnsWidth,
            align: 'center',
            fixed: 'right',
            resizable: false,
            showOverflow: 'tooltip'
        }
        newColumns: [],
        tableKey: null,
        // 导出配置项
        exportConfig: {
            // 默认选中类型
            type: 'xlsx',
            // 局部自定义类型
            types: ['xlsx', 'csv'],
            // 自定义数据量列表
            mode: 'all',
            modes: ['current', 'selected', 'all'],
            remote: true,
            exportMethod: this.exportMethod,
            filename: '未命名',
            sheetName: 'sheet1',
            columns: [],
            isFooter: true,
            isHeader: true,
            isMerge: true,
            isColgroup: true,
            useStyle: true,
            ...this.exportConfigProp
        },
        // 打印配置项
        printConfig: {
          mode: 'current',
          modes: ['current'],
          sheetName: '',
          isHeader: true,
          isColgroup: true,
          isFooter: true
        },
      // 单选框配置项
      radioConfig: {
        highlight: true,
        strict: false,
        trigger: 'row'
      },
      // 工具栏配置
      toolbarConfig: {
          size: 'mini',
          custom: {
              icon: 'vxe-icon-setting'
          },
          enabled: this.toolbarFun === false ? false : true,
          refresh: {
              queryMethod: this.toolbarFun
          },
          slots: {
              buttons: 'toolbar_buttons'
          }
      },
      sortList: [], // 排序后的columns
      sortTableData: [] // 排序后的全量数据
    }
  },
  watch: {
      data: {
          handler(newVal, oldVal) {
              if(newVal !== oldVal){
                this.handleData(newVal)
          },
          immediate: true,
          deep: true
      },
      columns: {
           handler(newVal, oldVal) {
              if(newVal !== oldVal){
              if(this.btns.length === 0) {
                  this.newColumns = newVal
              } else {
                  this.newColumns = [ ...newVal.concat(this.btnsColumn)]
              }
              this.handlerColumns(newVal)
          },
          immediate: true,
          deep: true
      }
  }
  mounted(){
      this.$nextTick(() => {
          this.initSetting()
      })
  },
  methods: {
    // 分页改变触发事件
    handlePageChange({ currentPage, pageSize }) {
      this.tablePage.pageIndex = currentPage
      this.tablePage.pageSize = pageSize
      this.$emit('pageChange', { pageIndex: currentPage, pageSize })
    },
    // 单元格点击事件
    rowClick({ row, rowIndex, columnIndex, column }) {
      Object.defineProperty(row, 'rowIndex', { // 给每一行添加不可枚举属性rowIndex来标识当前行
        value: rowIndex,
        writable: true,
        enumerable: false
      })
      // 筛除多选时单击触发操作
      if (!column.property) {
        return
      }
      this.$emit('rowClick', { row, rowIndex, columnIndex })
    },
    // 单元格双击事件
    rowdblClick({ row, rowIndex, column }) {
      // 筛除多选时单击触发操作
      if (!column.property) {
        return
      }
      this.$emit('rowdblClick', { row, rowIndex })
    },
    // 右键点击单元格菜单
    menuClick({ menu, row, column }) {
      this.$emit('menuClick', { menu, row, column })
    },
    // 获取表格数据集(包括插入的临时数据)
    getRecordset() {
      return this.$refs.xTable.getRecordset()
    },
    // 获取当前选中的行数据(用于单选框)
    getRadioRecord() {
      return this.$refs.xTable.getRadioRecord()
    },
    // 删除复选框选中的行数据
    removeCheckboxRow() {
      return this.$refs.xTable.removeCheckboxRow()
    },
    // 获取当前已选中的行数据(用于复选框)
    getCheckboxRecords() {
      return this.$refs.xTable.getCheckboxRecords()
    },
    // 只对 edit-config 配置时有效,单元格编辑状态下被关闭时会触发该事件
    editClosed(row, rowIndex, column) {
      this.$emit('editClosed', row, rowIndex, column)
    }
    // 多选操作勾选某行
    // setCheckboxRow(rows, checked) {
    //   if (rows.length === 0) { // 清除全部多选选择
    //     this.$refs.xTable.clearCheckboxRow()
    //   } else {
    //     this.$refs.xTable.setCheckboxRow(rows, checked)
    //   }
    // }
    // // 表格尾部
    // sumNum(list, field) {
    //   let count = 0
    //   let num = 0
    //   list.forEach(item => {
    //     count += Number(item[field])
    //     num = count.toFixed(0)
    //   })
    //   return num
    // },
    // meanNum(list, field) {
    //   let count = 0
    //   list.forEach(item => {
    //     count += Number(item[field])
    //   })
    //   return (count / list.length).toFixed(2)
    // },
    // // 表格尾部总计
    // footerMethod({ columns, data }) {
    //   // return this.footerData
    //   const sums = []
    //   columns.forEach((column, columnIndex) => {
    //     if (columnIndex === 0) {
    //       sums.push('总')
    //     } else {
    //       let sumCell = null
    //       switch (column.property) {
    //         case 'stCount':
    //         case 'Sales':
    //         case 'Trans':
    //         case 'lstCount':
    //         case 'lSales':
    //         case 'lTrans':
    //           sumCell = this.sumNum(data, column.property)
    //           break
    //         case 'AC':
    //         case 'lAC':
    //           sumCell = this.meanNum(data, column.property)
    //           break
    //       }
    //       sums.push(sumCell)
    //     }
    //   })
    //   // 返回一个二维数组的表尾合计
    //   return [sums]
    // }
  }
}
</script>

我也很无奈

javascript代码没写完, 后几天补上, 工作忙....

页面展示

1739261593644.jpg

后续会逐步介绍每个功能