在现代前端开发中,表格组件是数据展示的核心工具之一。Vxe-Table 是一款功能强大、性能优异的 Vue 表格组件,但在实际项目中,直接使用 Vxe-Table 可能会导致代码重复、维护困难等问题。因此,对 Vxe-Table 进行二次封装,可以显著提升开发效率和代码的可维护性。本文将分享如何对 Vxe-Table 进行二次封装,并提供一个实用的封装示例。
为什么要二次封装 Vxe-Table?
- 减少重复代码
项目中可能有多个表格,它们的配置项(如分页、排序、列定义)往往相似。通过封装,可以将通用逻辑提取出来,减少重复代码。 - 统一风格与行为
封装后可以统一表格的样式、分页逻辑、排序规则等,确保项目中的表格风格一致。 - 增强可维护性
将表格逻辑集中管理,后续修改或扩展时只需调整封装组件,而不需要修改每个使用表格的地方。 - 提升开发效率
封装后,开发者只需关注业务逻辑,无需重复配置表格的基础功能。
二次封装的核心思路
- 提取通用配置
将分页、排序、列定义等通用配置提取到封装组件中。 - 暴露必要的 Props 和 Events
通过 Props 接收外部数据(如表格数据、列配置),通过 Events 暴露内部事件(如分页变化、排序变化)。 - 支持自定义插槽
提供插槽支持,允许外部自定义表格内容(如表头、操作列)。 - 集成常用功能
封装分页、加载状态、空数据提示等常用功能。
以上来源于deepseek解答, 解答就仁者见仁 , 智者见智了
封装代码
以下代码是经过优化后的代码,
将我司项目所使用到的所有表格功能都封装进去了,
大概封装后功能的有
- 表格查询表单封装,统一样式
- toolbar工具栏插槽
- 前端自己处理分页,且兼容筛选,排序等
- 前端处理导出数据, 也可以通过接口导出
- vxe-table本身自带的渲染器功能
- vxe-table本身的其他功能, 例如单元格编辑等
几年前,写过一篇关于vex-table的封装文章了, 在这里: 一个基于vue功能强大的表格组件--vxe-table的二次封装(一)
二次封装示例
组件结构
以下是新版本基于 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代码没写完, 后几天补上, 工作忙....
页面展示
后续会逐步介绍每个功能