在 Vue2 中后台管理系统中,表格是数据展示的核心组件 ------ 用户列表、订单管理、数据统计等场景都离不开它。但重复编写表格结构、分页逻辑、筛选功能,不仅效率低下,还会导致代码冗余、维护困难。本文基于 Vue2 + Element UI,封装一个配置化、高复用、功能完备的通用表格组件,支持动态列、分页、排序、选择、树形结构等核心功能,让表格开发从 "重复编码" 变成 "配置化组装"。

一、封装目标与核心价值
1. 封装核心目标
- 支持 Element UI 表格核心特性(边框、斑马纹、固定列、树形结构等)
- 配置化生成表格列,无需重复编写
<el-table-column> - 内置分页组件,统一分页逻辑(页码切换、条数选择)
- 支持筛选区、选择列、序号列、操作列的灵活开关
- 支持自定义单元格渲染、操作列按钮,适配复杂业务场景
- 提供表格操作 API(展开 / 收起树形行、获取选中行等)
2. 相比原生开发的优势
| 对比维度 | 原生开发方式 | 通用表格封装方式 |
|---|---|---|
| 开发效率 | 重复编写表格 + 分页结构,效率低 | 配置化生成,核心功能一键启用 |
| 代码维护 | 表格结构分散,修改需改多处 | 配置集中管理,修改更便捷 |
| 功能统一性 | 分页、排序逻辑易不一致 | 统一封装,交互体验一致 |
| 扩展性 | 自定义单元格需单独编写 | 支持 render 函数 + 插槽,灵活扩展 |
| 易用性 | 需手动维护分页、选中状态 | 内置状态管理,对外暴露简洁 API |
二、技术选型
- 核心框架:Vue2(Options API)
- UI 组件库:Element UI(2.x 版本,表格 + 分页核心依赖)
- 样式预处理:SCSS(结构化编写样式,支持组件样式隔离)
三、通用表格组件完整实现(BaseTable)
1. 组件核心思路
通过 props 接收数据源 、列配置 、分页参数 等核心配置,内部集成 <el-table> 和 <el-pagination>,通过 v-for 遍历列配置动态渲染表格列,同时封装分页、排序、选择等逻辑,对外暴露统一事件和 API,满足不同业务场景需求。
2. 完整组件代码(BaseTable.vue)
js
<template>
<div class="base-table">
<!-- 筛选搜索区域(支持插槽自定义) -->
<div v-if="showSearch" class="filter-container">
<el-row :gutter="20">
<el-col :span="24">
<slot name="search"></slot> <!-- 筛选表单插槽,父组件自定义 -->
</el-col>
</el-row>
</div>
<!-- 表格主体 -->
<el-table
v-loading="loading"
ref="table"
:data="data"
:border="border"
:stripe="stripe"
:height="height"
:max-height="maxHeight"
:row-key="rowKey"
:tree-props="treeProps"
:default-sort="defaultSort"
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
@sort-change="handleSortChange" <!-- 排序事件 -->
@selection-change="handleSelectionChange" <!-- 选择事件 -->
@row-click="handleRowClick" <!-- 行点击事件 -->
>
<!-- 选择列(批量操作) -->
<el-table-column
v-if="showSelection"
type="selection"
width="55"
align="center"
:selectable="selectable" <!-- 自定义是否可选择 -->
/>
<!-- 序号列 -->
<el-table-column
v-if="showIndex"
type="index"
label="序号"
width="80"
align="center"
:index="getIndex" <!-- 自定义序号计算(支持分页) -->
/>
<!-- 动态列(核心配置) -->
<template v-for="(column, index) in columns">
<el-table-column
:key="column.prop || index"
v-bind="column" <!-- 透传列配置属性(width、fixed、label等) -->
:align="column.align || 'center'"
:min-width="column.minWidth || 100"
>
<template #default="scope"> <!-- 单元格内容插槽 -->
<template v-if="column.render">
<!-- 支持 render 函数自定义渲染(复杂单元格) -->
<render-content
:render="column.render"
:row="scope.row"
:index="scope.$index"
:column="column"
></render-content>
</template>
<template v-else-if="column.slotName">
<!-- 支持具名插槽自定义渲染(更灵活的自定义) -->
<slot :name="column.slotName" :row="scope.row" :index="scope.$index"></slot>
</template>
<template v-else>
<!-- 默认渲染:直接显示字段值(支持格式化) -->
{{ formatCellValue(scope.row[column.prop], column, scope.row) }}
</template>
</template>
</el-table-column>
</template>
<!-- 操作列(支持插槽自定义按钮) -->
<el-table-column
v-if="showOperation"
:label="$t('common.operation')" <!-- 国际化支持 -->
:width="operationWidth"
:min-width="operationWidth"
:align="operationAlign"
:fixed="operationFixed" <!-- 支持固定操作列 -->
class-name="small-padding fixed-width"
>
<template #default="scope">
<slot name="operation" :row="scope.row" :index="scope.$index"></slot>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div v-if="showPagination && total > 0" class="pagination-container">
<el-pagination
:background="background"
:current-page.sync="currentPage"
:page-sizes="pageSizes"
:page-size.sync="pageSize"
:layout="paginationLayout"
:total="total"
@size-change="handleSizeChange" <!-- 每页条数变化 -->
@current-change="handleCurrentChange" <!-- 页码变化 -->
/>
</div>
</div>
</template>
<script>
export default {
name: 'BaseTable',
props: {
// 1. 核心数据
data: {
type: Array,
required: true,
default: () => []
},
// 2. 列配置(核心)
columns: {
type: Array,
required: true,
default: () => []
},
// 3. 加载状态
loading: {
type: Boolean,
default: false
},
// 4. 表格样式
border: {
type: Boolean,
default: true
},
stripe: {
type: Boolean,
default: true
},
height: {
type: [String, Number],
default: null
},
maxHeight: {
type: [String, Number],
default: null
},
headerCellStyle: {
type: [Object, Function],
default: () => ({ background: '#f5f7fa', fontWeight: 'bold' })
},
cellStyle: {
type: [Object, Function],
default: () => ({})
},
// 5. 行配置
rowKey: {
type: String,
default: 'id' // 树形表格/选择列依赖的行唯一标识
},
treeProps: {
type: Object,
default: () => ({
children: 'children',
hasChildren: 'hasChildren'
})
},
// 6. 排序配置
defaultSort: {
type: Object,
default: () => ({}) // { prop: 'createTime', order: 'descending' }
},
// 7. 功能开关
showSearch: {
type: Boolean,
default: true // 是否显示筛选区
},
showSelection: {
type: Boolean,
default: false // 是否显示选择列
},
showIndex: {
type: Boolean,
default: false // 是否显示序号列
},
showPagination: {
type: Boolean,
default: true // 是否显示分页
},
showOperation: {
type: Boolean,
default: true // 是否显示操作列
},
// 8. 选择列配置
selectable: {
type: Function,
default: () => true // 自定义是否可选择(返回布尔值)
},
// 9. 分页配置
background: {
type: Boolean,
default: true // 分页按钮背景色
},
paginationLayout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper' // 分页布局
},
pageSizes: {
type: Array,
default: () => [10, 20, 30, 50] // 每页条数选项
},
total: {
type: Number,
default: 0 // 总条数
},
currentPage: {
type: Number,
default: 1 // 当前页码(支持.sync修饰符)
},
pageSize: {
type: Number,
default: 10 // 每页条数(支持.sync修饰符)
},
// 10. 操作列配置
operationWidth: {
type: [String, Number],
default: 230 // 操作列宽度
},
operationAlign: {
type: String,
default: 'center' // 操作列对齐方式
},
operationFixed: {
type: [String, Boolean],
default: false // 操作列是否固定('right'/'left'/false)
}
},
data() {
return {
selectedRows: [] // 选中的行数据
}
},
methods: {
/**
* 格式化单元格值(支持自定义格式化函数)
* @param {*} value - 单元格原始值
* @param {Object} column - 列配置
* @param {Object} row - 行数据
* @returns {*} 格式化后的值
*/
formatCellValue(value, column, row) {
if (column.formatter && typeof column.formatter === 'function') {
return column.formatter(value, row, column)
}
// 默认处理:null/undefined 显示为 '-'
return value === null || value === undefined ? '-' : value
},
/**
* 序号列计算(支持分页,显示全局序号)
* @param {Number} index - 当前页行索引
* @returns {Number} 全局序号
*/
getIndex(index) {
return (this.currentPage - 1) * this.pageSize + index + 1
},
/**
* 排序变化事件(对外暴露)
* @param {Object} sort - { prop: 排序字段, order: 排序方向 }
*/
handleSortChange(sort) {
this.$emit('sort-change', sort)
},
/**
* 选择变化事件(对外暴露选中行)
* @param {Array} selection - 选中的行数据数组
*/
handleSelectionChange(selection) {
this.selectedRows = selection
this.$emit('selection-change', selection)
},
/**
* 行点击事件(对外暴露)
* @param {Object} row - 点击的行数据
* @param {Object} event - 事件对象
* @param {Object} column - 点击的列配置
*/
handleRowClick(row, event, column) {
this.$emit('row-click', row, event, column)
},
/**
* 每页条数变化(对外暴露分页参数)
* @param {Number} val - 新的每页条数
*/
handleSizeChange(val) {
this.pageSize = val
this.$emit('pagination', { page: 1, limit: val }) // 条数变化时重置为第1页
this.$emit('update:pageSize', val) // 支持.sync修饰符
},
/**
* 当前页变化(对外暴露分页参数)
* @param {Number} val - 新的页码
*/
handleCurrentChange(val) {
this.currentPage = val
this.$emit('pagination', { page: val, limit: this.pageSize })
this.$emit('update:currentPage', val) // 支持.sync修饰符
},
/**
* 树形表格:展开/收起指定行
* @param {Object} row - 目标行数据
* @param {Boolean} expanded - 是否展开(true/false)
*/
toggleRowExpansion(row, expanded) {
this.$refs.table.toggleRowExpansion(row, expanded)
},
/**
* 树形表格:展开所有行
*/
expandAll() {
this.data.forEach(row => {
this.$refs.table.toggleRowExpansion(row, true)
})
},
/**
* 树形表格:收起所有行
*/
collapseAll() {
this.data.forEach(row => {
this.$refs.table.toggleRowExpansion(row, false)
})
},
/**
* 手动设置选中行(对外暴露API)
* @param {Array} rows - 需要选中的行数据数组
*/
setSelectedRows(rows) {
this.$refs.table.clearSelection()
rows.forEach(row => {
this.$refs.table.toggleRowSelection(row, true)
})
},
/**
* 清空选中行(对外暴露API)
*/
clearSelectedRows() {
this.$refs.table.clearSelection()
this.selectedRows = []
},
/**
* 刷新表格数据(保留分页状态)
* @param {Array} newData - 新的表格数据
*/
refreshData(newData) {
this.$set(this, 'data', newData)
}
},
// 注册 render 函数渲染组件(用于自定义单元格)
components: {
RenderContent: {
functional: true,
props: {
render: Function,
row: Object,
index: Number,
column: Object
},
render: (h, ctx) => {
// 调用父组件传递的 render 函数,传入 h、行数据、索引、列配置
return ctx.props.render(h, ctx.props.row, ctx.props.index, ctx.props.column)
}
}
},
// 监听 props 中的 currentPage 和 pageSize 变化(支持外部修改)
watch: {
currentPage: {
handler(val) {
this.$set(this, 'currentPage', val)
},
immediate: true
},
pageSize: {
handler(val) {
this.$set(this, 'pageSize', val)
},
immediate: true
}
}
}
</script>
<style lang="scss" scoped>
.base-table {
width: 100%;
box-sizing: border-box;
// 筛选区样式
.filter-container {
padding: 10px 0;
margin-bottom: 10px;
box-sizing: border-box;
}
// 表格样式优化
.el-table {
width: 100%;
box-sizing: border-box;
// 表头样式
th.el-table__cell {
text-align: center;
}
// 单元格样式
td.el-table__cell {
padding: 12px 0;
box-sizing: border-box;
}
// 固定列边框优化
.el-table__fixed-right {
height: 100% !important;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.05);
}
}
// 分页区样式
.pagination-container {
padding: 15px 0;
text-align: right;
box-sizing: border-box;
.el-pagination {
display: inline-block;
}
}
// 操作列按钮间距
.small-padding {
.el-button {
margin: 0 4px;
}
}
}
</style>
四、组件核心特性详解
1. 配置化列(columns)
columns 是表格的核心配置,每个元素对应一列,支持 Element UI 表格列的所有原生属性,同时扩展了自定义渲染能力:
| 属性名 | 类型 | 说明 | 必传 |
|---|---|---|---|
prop |
String | 列数据绑定的字段(与 data 中字段对应) | 是(默认渲染时) |
label |
String | 列标题文本 | 是 |
width |
String/Number | 列宽度 | 否 |
minWidth |
String/Number | 列最小宽度 | 否(默认 100) |
fixed |
String/Boolean | 列是否固定('left'/'right'/false) | 否 |
align |
String | 列对齐方式(left/center/right) | 否(默认 center) |
formatter |
Function | 单元格值格式化函数(value, row, column) | 否 |
render |
Function | 单元格自定义渲染函数(h, row, index, column) | 否 |
slotName |
String | 单元格自定义插槽名称 | 否 |
hidden |
Boolean | 是否隐藏该列(动态控制) | 否 |
2. 内置核心功能
(1)筛选区(showSearch)
通过 slot="search" 自定义筛选表单,与通用表单组件(BaseForm)搭配使用,完美实现 "筛选 + 表格" 一体化:
vue
<base-table :data="tableData" :columns="columns">
<template #search>
<base-form :formData="searchForm" :formItems="searchFormItems" inline />
</template>
</base-table>
(2)选择列(showSelection)
启用后显示复选框列,支持批量操作,通过 selectable 控制是否可选择某行,通过 selectedRows 或 selection-change 事件获取选中行:
javascript
// 只允许选中状态为"启用"的行
selectable(row) {
return row.status === 1
}
(3)序号列(showIndex)
自动计算全局序号(支持分页),无需手动维护,序号公式:(当前页-1)*每页条数 + 行索引 + 1。
(4)树形表格(treeProps)
通过 treeProps 配置树形结构字段,支持展开 / 收起所有行(expandAll()/collapseAll()),适配层级数据展示:
javascript
treeProps: {
children: 'children', // 子节点字段名
hasChildren: 'hasChildren' // 是否有子节点的标记字段
}
(5)分页功能(showPagination)
内置分页组件,支持页码切换、条数选择,通过 pagination 事件对外暴露分页参数(page/limit),无需手动维护分页状态。
3. 灵活的自定义能力
(1)单元格格式化(formatter)
简单的文本格式化(如状态转换、日期格式化),直接通过 formatter 函数实现:
javascript
columns: [
{
prop: 'status',
label: '状态',
formatter: (value) => {
const statusMap = { 1: '启用', 0: '禁用' }
return statusMap[value] || '-'
}
},
{
prop: 'createTime',
label: '创建时间',
formatter: (value) => {
return value ? new Date(value).toLocaleString() : '-'
}
}
]
(2)单元格自定义渲染(render 函数)
复杂的单元格内容(如按钮、标签、图片),通过 render 函数渲染:
javascript
columns: [
{
prop: 'tags',
label: '标签',
render: (h, row) => {
return h('div', row.tags.map(tag => {
return h('el-tag', { props: { type: 'primary', size: 'mini' } }, tag)
}))
}
},
{
prop: 'avatar',
label: '头像',
width: 80,
render: (h, row) => {
return h('el-image', {
props: { src: row.avatar, fit: 'cover' },
style: { width: '40px', height: '40px', borderRadius: '50%' }
})
}
}
]
(3)单元格插槽(slotName)
需要更灵活的自定义(如嵌套组件、复杂逻辑),通过具名插槽实现:
vue
<!-- 父组件中 -->
<base-table :data="tableData" :columns="columns">
<!-- 自定义"操作状态"列 -->
<template #operationStatus="scope">
<el-button
type="text"
:style="{ color: scope.row.status === 1 ? 'green' : 'red' }"
@click="handleStatusClick(scope.row)"
>
{{ scope.row.status === 1 ? '禁用' : '启用' }}
</el-button>
</template>
</base-table>
<!-- 列配置 -->
columns: [
{
label: '操作状态',
width: 100,
slotName: 'operationStatus' // 与插槽名称对应
}
]
(4)操作列自定义(operation 插槽)
操作列按钮完全自定义,通过 slot="operation" 传入,支持权限控制、按钮禁用等逻辑:
vue
<base-table :data="tableData" :columns="columns">
<template #operation="scope">
<el-button type="text" size="mini" @click="handleView(scope.row)">查看</el-button>
<el-button type="text" size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button
type="text"
size="mini"
type="danger"
@click="handleDelete(scope.row)"
:disabled="scope.row.status === 1"
>
删除
</el-button>
</template>
</base-table>
4. 对外暴露的 API
组件提供常用操作方法,通过 ref 调用:
| 方法名 | 说明 | 参数 |
|---|---|---|
toggleRowExpansion |
树形表格:展开 / 收起指定行 | row(行数据)、expanded(是否展开) |
expandAll |
树形表格:展开所有行 | - |
collapseAll |
树形表格:收起所有行 | - |
setSelectedRows |
手动设置选中行 | rows(行数据数组) |
clearSelectedRows |
清空选中行 | - |
refreshData |
刷新表格数据(保留分页状态) | newData(新数据数组) |
五、组件使用示例
1. 基础使用(简单列表 + 分页)
js
<template>
<div class="user-list">
<base-table
ref="userTable"
:data="tableData"
:columns="columns"
:total="total"
:loading="loading"
:show-index="true"
@pagination="handlePagination"
@sort-change="handleSortChange"
>
<!-- 筛选区 -->
<template #search>
<el-row :gutter="20">
<el-col :span="6">
<el-input
v-model="searchForm.username"
placeholder="请输入用户名"
size="mini"
style="width: 100%"
/>
</el-col>
<el-col :span="6">
<el-select
v-model="searchForm.status"
placeholder="请选择状态"
size="mini"
style="width: 100%"
>
<el-option label="全部" value="" />
<el-option label="启用" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-col>
<el-col :span="4">
<el-button type="primary" size="mini" @click="handleSearch">查询</el-button>
<el-button size="mini" @click="handleReset">重置</el-button>
</el-col>
</el-row>
</template>
<!-- 操作列 -->
<template #operation="scope">
<el-button type="text" size="mini" @click="handleView(scope.row)">查看</el-button>
<el-button type="text" size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</base-table>
</div>
</template>
<script>
import BaseTable from '@/components/BaseTable'
export default {
components: { BaseTable },
data() {
return {
loading: false,
tableData: [],
total: 0,
searchForm: {
username: '',
status: ''
},
// 列配置
columns: [
{
prop: 'username',
label: '用户名',
width: 120
},
{
prop: 'email',
label: '邮箱',
width: 180
},
{
prop: 'gender',
label: '性别',
width: 80,
formatter: (value) => {
return value === 1 ? '男' : value === 2 ? '女' : '未知'
}
},
{
prop: 'status',
label: '状态',
width: 80,
formatter: (value) => {
return value === 1 ? '<span style="color: green">启用</span>' : '<span style="color: red">禁用</span>'
},
// 支持HTML渲染
props: {
raw: true
}
},
{
prop: 'createTime',
label: '创建时间',
width: 180,
sortable: true, // 启用排序
formatter: (value) => {
return value ? new Date(value).toLocaleString() : '-'
}
}
]
}
},
mounted() {
this.fetchData() // 初始化加载数据
},
methods: {
// 加载数据
async fetchData() {
this.loading = true
try {
const params = {
page: this.$refs.userTable.currentPage,
limit: this.$refs.userTable.pageSize,
username: this.searchForm.username,
status: this.searchForm.status,
// 排序参数(从sort-change事件获取)
...this.sortParams
}
const res = await this.$api.user.getUserList(params)
this.tableData = res.data.list
this.total = res.data.total
} catch (error) {
console.error('加载用户列表失败:', error)
} finally {
this.loading = false
}
},
// 分页变化
handlePagination({ page, limit }) {
this.fetchData()
},
// 排序变化
handleSortChange(sort) {
this.sortParams = sort.order ? {
sortField: sort.prop,
sortOrder: sort.order === 'ascending' ? 'asc' : 'desc'
} : {}
this.fetchData()
},
// 搜索
handleSearch() {
// 搜索时重置为第1页
this.$refs.userTable.currentPage = 1
this.fetchData()
},
// 重置
handleReset() {
this.searchForm = { username: '', status: '' }
this.$refs.userTable.currentPage = 1
this.sortParams = {}
this.fetchData()
},
// 查看
handleView(row) {
console.log('查看用户:', row)
// 打开查看弹窗...
},
// 编辑
handleEdit(row) {
console.log('编辑用户:', row)
// 打开编辑弹窗...
},
// 删除
handleDelete(row) {
this.$confirm('确定要删除该用户吗?', '提示', {
type: 'warning'
}).then(async () => {
try {
await this.$api.user.deleteUser(row.id)
this.$message.success('删除成功')
this.fetchData()
} catch (error) {
this.$message.error('删除失败')
}
})
}
}
}
</script>
2. 进阶使用(树形表格 + 选择列 + 自定义渲染)
js
<template>
<div class="dept-list">
<base-table
ref="deptTable"
:data="tableData"
:columns="columns"
:total="total"
:loading="loading"
:show-selection="true"
:tree-props="treeProps"
:selectable="handleSelectable"
@selection-change="handleSelectionChange"
>
<!-- 操作列 -->
<template #operation="scope">
<el-button type="text" size="mini" @click="handleAdd(scope.row)">新增子部门</el-button>
<el-button type="text" size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button
type="text"
size="mini"
type="danger"
@click="handleDelete(scope.row)"
:disabled="scope.row.hasChildren"
>
删除
</el-button>
</template>
</base-table>
</div>
</template>
<script>
import BaseTable from '@/components/BaseTable'
export default {
components: { BaseTable },
data() {
return {
loading: false,
tableData: [],
total: 0,
treeProps: {
children: 'children',
hasChildren: 'hasChildren'
},
columns: [
{
prop: 'deptName',
label: '部门名称',
width: 200
},
{
prop: 'deptCode',
label: '部门编码',
width: 150
},
{
prop: 'leader',
label: '负责人',
width: 120
},
{
prop: 'userCount',
label: '用户数',
width: 100
},
{
prop: 'status',
label: '状态',
width: 100,
render: (h, row) => {
return h('el-switch', {
props: {
value: row.status === 1,
disabled: true
},
scopedSlots: {
open: () => h('span', '启用'),
close: () => h('span', '禁用')
}
})
}
}
]
}
},
mounted() {
this.fetchData()
},
methods: {
// 加载树形部门数据
async fetchData() {
this.loading = true
try {
const res = await this.$api.dept.getDeptTree()
this.tableData = res.data
this.total = res.data.length // 树形结构无需分页,总条数为根节点数
} catch (error) {
console.error('加载部门树失败:', error)
} finally {
this.loading = false
}
},
// 自定义选择规则:不允许选择有子部门的行
handleSelectable(row) {
return !row.hasChildren
},
// 选中行变化
handleSelectionChange(selection) {
console.log('选中的部门:', selection)
// 批量操作逻辑...
},
// 新增子部门
handleAdd(parentDept) {
console.log('新增子部门,父部门:', parentDept)
// 打开新增弹窗...
},
// 编辑部门
handleEdit(dept) {
console.log('编辑部门:', dept)
// 打开编辑弹窗...
},
// 删除部门
handleDelete(dept) {
this.$confirm('确定要删除该部门吗?', '提示', { type: 'warning' }).then(async () => {
try {
await this.$api.dept.deleteDept(dept.id)
this.$message.success('删除成功')
this.fetchData()
} catch (error) {
this.$message.error('删除失败')
}
})
}
}
}
</script>