跳转至

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