Vue3 二次封装 Element Plus 通用配置化表格全量解决方案
原文地址: https://88box.top 生成时间: 2026-05-19 16:33:52
Vue3 二次封装 Element Plus 通用配置化表格全量封装 - hey99 知识搜索引擎
精选文章
Vue3 二次封装 Element Plus 通用配置化表格全量封装
本文基于 Vue3 与 Element Plus 封装通用配置化表格组件,整合分页、全局序号、自适应高度与多场景插槽能力。简化后台列表开发,统一页面样式,大幅减少重复代码,适配多数中后台业务场景。
更新于 2026-05-19 08:17
前端
Vue.js
TypeScript
引言
在后台管理系统开发中,表格是最常用的 UI 组件之一。Element Plus 提供了功能强大的 el-table 组件,但在实际项目中,我们往往需要根据业务需求进行二次封装,以提高开发效率和代码复用性。
本文将详细介绍如何基于 Element Plus 封装一个功能完善、易于扩展的表格组件。
功能特性
支持自定义列配置
内置分页功能
支持加载状态
支持空状态定制
支持全局序号列
支持插槽自定义
实现细节
Props
参数
说明
类型
默认值
可选值
data
表格数据
Array
[]
-
columns
列配置
Array
[]
-
loading
加载状态
Boolean
false
true/false
pagination
分页配置
Object/Boolean
{}
-
paginationOptions
分页组件配置
Object
{}
-
emptyHeight
空数据表格高度
String
"100%"
-
emptyOptions
空状态组件配置
Object
{}
-
fit
列宽是否自适应
Boolean
true
true/false
showHeader
是否显示表头
Boolean
true
true/false
stripe
是否显示斑马纹
Boolean
-
true/false
border
是否显示边框
Boolean
-
true/false
size
表格尺寸
String
-
medium/small/mini
height
表格高度
String/Number
-
-
其他配置
支持 Element UI el-table 的所有配置
-
Events
事件名称
说明
回调参数
pagination:size-change
每页条数变化时触发
size
pagination:current-change
当前页码变化时触发
current
其他事件
支持 Element UI el-table 的所有事件
同 Element UI
Column 配置项
参数
说明
类型
默认值
可选值
prop
列字段名
String
-
-
label
列标题
String
-
-
width
列宽
String/Number
-
-
minWidth
最小列宽
String/Number
-
-
fixed
是否固定列
String/Boolean
-
left/right/true/false
type
列类型
String
-
globalIndex/index/selection/expand
hidden
是否隐藏列
Boolean
false
true/false
其他配置
支持 Element UI el-table-column 的所有配置
-
Slots
插槽名称
说明
tableEmpty
空状态插槽
tableEmptyDescription
空状态描述插槽
tableEmptyImage
空状态图片插槽
tableEmptyDefault
空状态默认插槽
tableAppend
表格底部追加内容插槽
tablePaginationDefault
分页组件默认插槽
[prop]-header
自定义列头插槽,[prop] 为列的 prop 值
[prop]
自定义列内容插槽,[prop] 为列的 prop 值
组件源码
LdTableColumn
<
template
<
el-table-column
v-bind
=
"column"
<
template
v-if
=
"column.children && column.children.length > 0"
<
ld-table-column
v-for
=
"childCol in column.children"
:key
=
"childCol.prop || childCol.label"
:column
=
"childCol"
/>
</
template
<
template
v-if
=
"
(!column.children || column.children.length === 0) && column.useHeaderSlot && column.prop
"
header
=
"{ column: elColumn, $index }"
<
slot
:name
=
"${column.prop}-header"
:column
=
"elColumn"
:index
=
"$index"
{{ column.label }}
</
slot
</
template
<
template
v-if
=
"(!column.children || column.children.length === 0) && column.useSlot && column.prop"
default
=
"{ row, column: elColumn, $index }"
<
slot
:name
=
"column.prop"
:row
=
"row"
:column
=
"elColumn"
:index
=
"$index"
{{
column.formatter
? column.formatter(row, elColumn, row[column.prop], $index)
: row[column.prop]
}}
</
slot
</
template
</
el-table-column
</
template
LdTable
<
template
<
div
class
=
"ld-table"
:class
=
"{ 'ld-table--empty': isEmpty }"
:style
=
"containerHeight"
<
el-table
ref
=
"elTableRef"
v-loading
=
"!!loading"
v-bind
=
"mergedTableProps"
<
template
v-for
=
"col in tableColumns"
<
el-table-column
v-if
=
"col.type === 'globalIndex'"
v-bind
=
"{ ...col }"
:key
=
"'globalIndex-' + (col.prop || col.type)"
:fixed
=
"col.fixed || 'left'"
:align
=
"col.align || 'center'"
:header-align
=
"col.headerAlign || 'center'"
<
template
header
=
"{ column, $index }"
<
slot
:name
=
"${column.prop}-header"
:column
=
"column"
:index
=
"$index"
<
span
{{ col.label || '序号' }}
</
span
</
slot
</
template
<
template
default
=
"{ row, column, $index }"
<
slot
:name
=
"${column.prop}"
:row
=
"row"
:column
=
"column"
:index
=
"$index"
<
span
{{ getGlobalIndex($index) }}
</
span
</
slot
</
template
</
el-table-column
<
ld-table-column
v-else
:key
=
"col.prop || col.label"
:column
=
"col"
<
template
v-for
=
"(_, slotName) in slots"
[
slotName
]=
"slotProps"
<
slot
:name
=
"slotName"
v-bind
=
"slotProps"
/>
</
template
</
ld-table-column
</
template
<
template
v-if
=
"slots['default']"
default
<
slot
</
slot
</
template
<
template
empty
<
slot
name
=
"tableEmpty"
<
div
v-if
=
"loading"
</
div
<
el-empty
v-else
v-bind
=
"mergedEmptyOptions"
<
template
v-if
=
"slots['tableEmptyDescription']"
description
<
slot
name
=
"tableEmptyDescription"
</
slot
</
template
<
template
v-if
=
"slots['tableEmptyImage']"
image
<
slot
name
=
"tableEmptyImage"
</
slot
</
template
<
template
v-if
=
"slots['tableEmptyDefault']"
default
<
slot
name
=
"tableEmptyDefault"
</
slot
</
template
</
el-empty
</
slot
</
template
<
template
append
<
slot
name
=
"tableAppend"
</
slot
</
template
</
el-table
<
div
v-if
=
"showPagination"
ref
=
"paginationRef"
class
=
"ld-table__pagination ld-table__pagination--custom"
:class
=
"'ld-table__pagination--' + mergedPaginationOptions.align"
<
el-pagination
v-bind
=
"mergedPaginationOptions"
:total
=
"pagination?.total"
:disabled
=
"loading"
:page-size
=
"pagination?.pageSize"
:current-page
=
"pagination?.currentPage"
@
size-change
=
"handleSizeChange"
@
current-change
=
"handleCurrentChange"
@
change
=
"handlePaginationChange"
@
prev-click
=
"handlePrevClick"
@
next-click
=
"handleNextClick"
<
template
v-if
=
"slots['tablePaginationDefault']"
default
<
slot
name
=
"tablePaginationDefault"
</
slot
</
template
</
el-pagination
</
div
</
div
</
template
<
style
lang
=
"scss"
scoped
.ld-table
{
width
:
100%
;
position
: relative;
- {
margin
:
0
;
padding
:
0
;
box-sizing
: border-box;
}
.el-table
{
width
:
100%
;
}
.ld-table__pagination
{
margin-top
:
13px
;
&
.ld-table__pagination--custom
{
display
: flex;
align-items
: center;
flex-wrap
: wrap;
gap
:
10px
;
}
&
.ld-table__pagination--left
{
justify-content
: flex-start;
}
&
.ld-table__pagination--center
{
justify-content
: center;
}
&
.ld-table__pagination--right
{
justify-content
: flex-end;
}
}
}
</
style
类型注解
import
type
{
EmptyProps
,
PaginationProps
,
TableColumnCtx
,
TableProps
}
from
'element-plus'
;
import
type
{
VNode
}
from
'vue'
;
export
interface
LdTableEmits
{
(
e
:
'pagination:current-change'
,
current
:
number
):
void
;
(
e
:
'pagination:size-change'
,
size
:
number
):
void
;
(
e
:
'pagination:change'
,
pageSize
:
number
,
currentPage
:
number
):
void
;
(
e
:
'pagination:prev-click'
,
currentPage
:
number
):
void
;
(
e
:
'pagination:next-click'
,
currentPage
:
number
):
void
;
}
export
interface
LdTableSlots
{
default
:
(
scope:
unknown
) =>
VNode
[];
tablePaginationDefault
:
(
scope:
unknown
) =>
VNode
[];
tableEmpty
:
(
scope:
unknown
) =>
VNode
[];
tableEmptyDefault
:
(
scope:
unknown
) =>
VNode
[];
tableEmptyDescription
:
(
scope:
unknown
) =>
VNode
[];
tableEmptyImage
:
(
scope:
unknown
) =>
VNode
[];
tableAppend
:
(
scope:
unknown
) =>
VNode
[];
[
key
:
string
]:
(
scope:
unknown
) =>
VNode
[];
}
export
interface
LdTablePaginationConfig
{
// 总页数
total
:
number
;
// 当前页码
currentPage
:
number
;
// 每页显示条数
pageSize
:
number
;
}
// 表格列配置接口
export
interface
ColumnOption
<T =
unknown
{
// 列类型
type
?:
'selection'
|
'expand'
|
'index'
|
'globalIndex'
;
// 列属性名
prop?:
string
;
// 列标题
label?:
string
;
// 列宽度
width?:
string
|
number
;
// 最小列宽度
minWidth?:
string
|
number
;
// 固定列
fixed?:
boolean
|
'left'
|
'right'
;
// 是否可排序
sortable?:
boolean
;
// 过滤器选项
filters?:
unknown
[];
// 过滤方法
filterMethod?:
(
value:
unknown
, row: T
) =>
boolean
;
// 过滤器位置
filterPlacement?:
string
;
// 是否禁用
disabled?:
boolean
;
// 是否显示列
visible?:
boolean
;
// 是否选中显示
checked?:
boolean
;
// 自定义渲染函数
formatter?:
(
row: T
) =>
unknown
;
// 插槽相关配置
// 是否使用插槽渲染内容
useSlot?:
boolean
;
// 插槽名称(默认为 prop 值)
slotName?:
string
;
// 是否使用表头插槽
useHeaderSlot?:
boolean
;
// 表头插槽名称(默认为 ${prop}-header)
headerSlotName?:
string
;
// 其他属性
[
key
:
string
]:
unknown
;
}
export
type
TableSize
=
'large'
|
'default'
|
'small'
;
export
interface
LdTableColumnProps
extends
Partial
<
TableColumnCtx
<
Record
<
string
,
unknown
{
prop?:
string
;
label?:
string
;
type
?:
string
;
hidden?:
boolean
;
fixed?:
'left'
|
'right'
|
boolean
;
align?:
'left'
|
'center'
|
'right'
;
headerAlign?:
'left'
|
'center'
|
'right'
;
}
export
interface
LdTableConfig
{
columns
:
LdTableColumnProps
[];
loading?:
boolean
;
pagination?:
LdTablePaginationConfig
;
paginationOptions?:
PaginationProps
;
emptyHeight?:
string
|
number
;
emptyOptions?:
EmptyProps
;
fit?:
boolean
;
showHeader?:
boolean
;
stripe?:
boolean
;
border?:
boolean
;
size?:
TableSize
;
height?:
string
|
number
;
emptyText?:
string
;
}
export
type
LdTableProps
=
LdTableConfig
&
Partial
<
TableProps
<
Record
<
string
,
unknown
;
export
type
LdTablePaginationProps
=
PaginationProps
& {
align?:
'left'
|
'center'
|
'right'
;
};
工具 hooks
import
{
computed,
toValue,
watch,
getCurrentScope,
onScopeDispose,
type
MaybeRefOrGetter
,
}
from
'vue'
;
function
unrefElement
(
el: MaybeRef<HTMLElement | SVGElement | ComponentPublicInstance |
undefined
|
null
,
) {
const
_el =
toValue
(el);
return
(_el
as
ComponentPublicInstance
)?.
$el
?? _el;
}
export
const
useResizeObserver
= (
target:
| MaybeRefOrGetter<HTMLElement | SVGElement | ComponentPublicInstance |
null
|
undefined
| MaybeRefOrGetter<HTMLElement | SVGElement | ComponentPublicInstance |
null
|
undefined
[],
callback: ResizeObserverCallback,
options: ResizeObserverOptions = {},
) => {
let
observer
:
ResizeObserver
|
undefined
;
const
isSupported =
computed
(
() =>
window
&&
'ResizeObserver'
in
window
);
const
cleanup
= (
) => {
if
(observer) {
observer.
disconnect
();
observer =
undefined
;
}
};
const
targets =
computed
(
() =>
{
const
_targets =
toValue
(target);
return
Array
.
isArray
(_targets)
? _targets.
map
(
(
el
) =>
unrefElement
(el
as
ComponentPublicInstance
))
: [
unrefElement
(_targets)];
});
const
stopWatch =
watch
(
targets,
(
els
) =>
{
cleanup
();
if
(isSupported.
value
&&
window
) {
observer =
new
ResizeObserver
(callback);
for
(
const
_el
of
els) {
if
(_el) observer.
observe
(_el, options);
}
}
},
{
immediate
:
true
,
flush
:
'post'
},
);
const
stop
= (
) => {
cleanup
();
stopWatch
();
};
if
(
getCurrentScope
()) {
onScopeDispose
(stop);
}
return
{
isSupported,
stop,
};
};
FAQ
当使用
pagination
功能时,需要传入正确的
current
、
size
和
total
值
当使用
globalIndex
类型的列时,会自动计算全局序号,不受分页影响
当表格数据为空且非加载状态时,会显示空状态组件
TODO:
render
函数来定义列渲染
感谢阅读,敬请斧正!
查看原文
🏷 标签: Vue3, Element Plus, 组件封装, 配置化表格, TypeScript