【Vue3】基于 Element-Plus 的增强型表格组件封装实践
·
基于 Element-Plus 的增强型表格组件封装实践
Element Plus提供了功能丰富的el-table,但在复杂业务场景下,直接使用原生表格组件会导致大量重复代码,封装一个增强型表格组件(EnhancedTable),解决项目中表格使用痛点,提升开发效率。
为什么需要封装表格组件?
在实际项目开发中,直接使用Element Plus表格存在以下问题:
- 重复代码多:每个页面都需要处理分页、加载状态、列配置等基础逻辑
- 功能扩展难:新增功能(如列设置、自定义操作栏)需要在每个表格重复实现
- 代码维护成本高:表格逻辑分散在各处,修改一处需多处调整
- UI风格不统一:不同开发人员实现的表格样式和交互存在差异
通过封装EnhancedTable组件,我们实现了一次封装,多处复用,显著提升了开发效率和用户体验一致性。
关键实现细节
1. 类型定义与Props设计
interface TableColumn extends Partial<TableColumnCtx<any>> {
prop: string
label: string
minWidth?: string | number
sortable?: boolean | string
filters?: any[]
filterMethod?: (value: any, row: any, column: TableColumnCtx<any>) => boolean
slot?: boolean // 标记是否使用自定义插槽
disabledColumnSetting?: boolean
fixed?: boolean | string
}
interface EnhancedTableProps {
tableLoading: boolean
data: any[]
columns: TableColumn[]
defaultVisibleColumns?: string[]
showHeader?: boolean
showSelection?: boolean
indexConfig?: IndexConfig
showPagination?: boolean
}
- 继承
TableColumnCtx但仅保留必要属性,避免类型不匹配问题 - 添加
slot属性标记自定义列,解决原生表格插槽使用不便问题 - 通过
IndexConfig精细化控制序号列显示
2. 属性合并与透传
const attrs = useAttrs()
const mergedTableProps = computed(() => {
const { paginationAttrs, ...tableAttrs } = attrs
return {
...props,
...tableAttrs
}
})
- 支持将额外属性透传给底层
el-table - 避免属性冲突(特别处理了
paginationAttrs) - 保持组件
API的灵活性
3. 列可见性控制
const visibleColumns = ref<string[]>(...defaultVisibleColumns)
const filteredColumns = computed(() => {
const visibleSet = new Set(visibleColumns.value)
return props.columns.filter(col => visibleSet.has(col.prop))
})
- 支持动态显示/隐藏列(为后续列设置功能打基础)
- 通过计算属性实现高效更新
- 保持数据源与展示层分离
4. 分页状态管理
const pageInfo = defineModel('pageInfo', {
type: Object as PropType<PageInfo>,
default: () => ({ currentPage: 1, pageSize: 10, total: 100 })
})
const updatePageInfo = debounce({ delay: 300 }, () => emits('updatePageInfo'))
- 使用
defineModel实现v-model双向绑定 - 添加防抖机制避免频繁请求
- 提供默认分页配置,减少重复代码
5. 插槽机制设计
<template v-for="column in filteredColumns" :key="column.prop">
<el-table-column v-bind="column">
<template #default="scope" v-if="column.slot">
<slot :name="column.prop" v-bind="scope"></slot>
</template>
</el-table-column>
</template>
解决痛点:
- 通过
column.slot标记简化自定义列配置 - 保持与
Element Plus插槽API兼容 - 使模板代码更简洁直观
使用示例
基础用法
<EnhancedTable
:data="tableData"
:columns="columns"
:paginationAttrs="paginationAttrs"
v-model:pageInfo="pagination"
@updatePageInfo="fetchData"
>
<!-- 自定义状态列 -->
<template #status="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
{{ row.status }}
</el-tag>
</template>
<!-- 自定义操作列 -->
<template #action="{ row }">
<el-button type="primary" @click="edit(row)">编辑</el-button>
</template>
<!-- 自定义工具栏 -->
<template #toolbar>
<el-input v-model="searchQuery" placeholder="搜索..." />
<el-button type="primary" @click="fetchData">搜索</el-button>
</template>
</EnhancedTable>
高级配置
// 列配置示例
const columns = ref<CustomColumn[]>([
{
property: 'name',
label: '姓名',
align: 'center',
width: '120',
sortable: true
},
{
property: 'status',
label: '状态',
width: '100',
slot: true,
filterMethod: (value, row) => row.status === value
},
{
property: 'action',
label: '操作',
width: '120',
slot: true,
fixed: 'right'
}
])
// 分页配置
const paginationAttrs = {
background: true,
pageSizes: [10, 20, 50],
layout: 'total, sizes, prev, pager, next'
}
未来优化方向
- 列设置功能:添加列显示/隐藏的UI控制
- 表格配置持久化:保存用户自定义的列设置
- 虚拟滚动支持:优化大数据量场景性能
- 导出功能集成:内置Excel导出能力
- 响应式布局:适配不同屏幕尺寸
总结
组件EnhancedTable完整代码
<template>
<div class="enhanced-table-container">
<!-- 表格顶部操作区域 -->
<div v-if="$slots.toolbar" class="w-full flex justify-between items-center mb-4">
<slot name="toolbar"></slot>
</div>
<!-- 主表格 -->
<el-table
ref="tableRef"
v-loading="tableLoading"
v-bind="mergedTableProps"
:data="tableData"
:show-header="showHeader"
>
<!-- 选择列 -->
<el-table-column
v-if="showSelection"
type="selection"
width="55"
align="center"
fixed="left"
/>
<!-- 序号列 -->
<el-table-column
v-if="indexConfig.showIndex"
:label="indexConfig.label ?? '序号'"
:width="indexConfig.width ?? '80'"
:fixed="indexConfig.fixed ?? false"
type="index"
align="center"
/>
<!-- 动态列 -->
<template v-for="column in filteredColumns" :key="column.prop">
<el-table-column
v-bind="column"
:prop="column.prop"
:label="column.label"
:min-width="column.minWidth"
:sortable="column.sortable || false"
:filters="column.filters"
:filter-method="column.filterMethod"
>
<!-- 自定义列内容 -->
<template #default="scope" v-if="column.slot">
<slot :name="column.prop" v-bind="scope"></slot>
</template>
</el-table-column>
</template>
<!-- 自定义插槽 -->
<slot></slot>
</el-table>
<!-- 分页区域 -->
<div v-if="showPagination" class="flex justify-end mt-4">
<el-pagination
ref="paginationRef"
v-bind="paginationAttrs"
:total="pageInfo.total"
:disabled="tableLoading"
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
@update:current-page="updatePageInfo"
@update:page-size="updatePageInfo"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { TableInstance, TableColumnCtx, ElPagination, PaginationProps } from 'element-plus'
import { debounce } from 'radashi'
// 定义类型
interface TableColumn extends Partial<TableColumnCtx<any>> {
prop: string
label: string
minWidth?: string | number
sortable?: boolean | string
filters?: any[]
filterMethod?: (value: any, row: any, column: TableColumnCtx<any>) => boolean
slot?: boolean
disabledColumnSetting?: boolean
fixed?: boolean | string
}
interface IndexConfig {
showIndex?: boolean // 是否显示序号列
label?: string // 序号列标签
width?: string | number // 序号列宽度
fixed?: boolean | string // 是否固定列
}
interface EnhancedTableProps {
tableLoading: boolean // 表格加载状态
data: any[] // 表格数据
columns: TableColumn[] // 列配置
defaultVisibleColumns?: string[] // 默认显示的列(用于列设置)
showHeader?: boolean // 是否显示表头
showSelection?: boolean // 是否显示选择列
indexConfig?: IndexConfig // 序号列
showPagination?: boolean // 是否显示分页
}
const props = withDefaults(defineProps<EnhancedTableProps>(), {
tableLoading: false,
data: () => [],
columns: () => [],
defaultVisibleColumns: () => [],
showHeader: true,
showSelection: false,
indexConfig: () => ({
showIndex: false
}),
showPagination: true,
})
// 合并props和attrs Table用
const attrs = useAttrs()
const mergedTableProps = computed(() => {
// paginationAttrs作为Paginantion组件使用的属性,所以要排除
const { paginationAttrs, ...tableAttrs } = attrs
return {
...props,
...tableAttrs
}
})
// 表格引用
const tableRef = ref<TableInstance | null>(null)
// 表格数据
const tableData = computed(() => props.data)
// 可见列
const visibleColumns = ref<string[]>(
props.defaultVisibleColumns.length > 0
? [...props.defaultVisibleColumns]
: props.columns.map(col => col.prop)
)
// 过滤后的列
const filteredColumns = computed(() => {
const visibleSet = new Set(visibleColumns.value)
return props.columns.filter(col => visibleSet.has(col.prop))
})
// pagination用
const paginationRef = ref<InstanceType<typeof ElPagination> | null>(null)
const paginationAttrs = computed(() => {
const defaultAttrs = {
background: true,
hideOnSinglePage: true,
pageSizes: [10, 20, 50, 100],
layout: 'total, sizes, prev, pager, next, jumper',
} as PaginationProps;
const baseAttrs = attrs.paginationAttrs;
const validAttrs = typeof baseAttrs === 'object' && baseAttrs !== null
? baseAttrs
: {};
return {
...defaultAttrs,
...validAttrs,
} as PaginationProps;
})
const emits = defineEmits<{
(e: 'updatePageInfo'): void
}>()
interface PageInfo {
currentPage: number
pageSize: number
total: number
}
const pageInfo = defineModel('pageInfo', {
type: Object as PropType<PageInfo>,
default: () => ({
currentPage: 1,
pageSize: 10,
total: 100
})
})
const updatePageInfo = debounce(
{ delay: 300 },
() => emits('updatePageInfo')
)
// 暴露方法
defineExpose({
// 获取表格实例
getTableRef: () => tableRef.value,
/**
* TypeScript 对 Vue 组件实例类型推导的限制,只暴露明确需要的 API,而非整个组件实例,否则打包时会报错,
* 也可通过修改tsconfig.json避免报错: "compilerOptions": { "verbatimModuleSyntax": false, "isolatedModules": false }
* ...tableRef.value ? tableRef.value : {},
*/
// 其它需要暴露的API
})
</script>
完整使用示例
<template>
<EnhancedTable
:data="tableData"
:columns="columns"
:tableLoading="fetchLoading"
:toolbarOptions="toolbarOptions"
:border="true"
:show-selection="true"
:show-index="true"
:index-config="indexConfig"
:scrollbar-always-on="true"
:paginationAttrs="paginationAttrs"
v-model:pageInfo="pagination"
@updatePageInfo="fetchData"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<!-- slot使用示例:自定义状态列 -->
<template #status="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
{{ row.status }}
</el-tag>
</template>
<!-- slot使用示例:自定义操作列 -->
<template #action="{ row }">
<div class="flex items-center">
<el-button type="primary" :icon="Edit" circle />
<el-button type="danger" :icon="Delete" circle />
</div>
</template>
<!-- 自定义表头 -->
<template #toolbar>
<div>
<el-input
v-model="searchQuery"
placeholder="搜索..."
style="width: 200px;"
/>
<el-button type="primary" @click="fetchData">Search</el-button>
</div>
<div class="flex items-center gap-2">
<el-button type="primary">新增</el-button>
<el-button type="primary">导出</el-button>
</div>
</template>
</EnhancedTable>
</template>
<script setup lang="ts">
import { Edit, Delete } from '@element-plus/icons-vue'
import type { TableColumnCtx } from 'element-plus'
const EnhancedTable = defineAsyncComponent(() => import('@/components/basic/EnhancedTable/index.vue'))
const searchQuery = ref<string>('')
const indexConfig = ref({
showIndex: true,
fixed: 'left'
})
const toolbarOptions = {
refresh: true,
columnSetting: true
}
interface User {
id: number
name: string
age: number
[k: string]: any
status: string
}
// 表格数据
const tableData = ref<User[]>([])
// 列配置
const columns = [
{ prop: 'name', label: '姓名', align: 'center', minWidth: '120' },
{ prop: 'status', label: '状态', align: 'center', width: '100',
slot: true,
filters: [
{ text: 'Active', value: 'active' },
{ text: 'Inactive', value: 'inactive' }
],
filterMethod: (value: string, row: any, column: TableColumnCtx<User>) =>{
const property = column['property']
return row[property] === value
},
filterPlacement: 'bottom-end'
},
{ prop: 'age', label: '年龄', align: 'center', width: '100', sortable: true },
{ prop: 'age1', label: '年龄1', align: 'center', width: '200', sortable: true },
{ prop: 'age2', label: '年龄2', align: 'center', width: '200', sortable: true },
{ prop: 'age3', label: '年龄3', align: 'center', width: '200', sortable: true },
{ prop: 'age4', label: '年龄4', align: 'center', width: '200', sortable: true },
{ prop: 'age5', label: '年龄5', align: 'center', width: '200', sortable: true },
{ prop: 'age6', label: '年龄6', align: 'center', width: '200', sortable: true },
{ prop: 'age61', label: '年龄61', align: 'center', width: '200', sortable: true },
{ prop: 'age62', label: '年龄62', align: 'center', width: '200', sortable: true },
{ prop: 'age63', label: '年龄63', align: 'center', width: '200', sortable: true },
{ prop: 'age64', label: '年龄64', align: 'center', width: '200', sortable: true },
{ prop: 'age65', label: '年龄65', align: 'center', width: '200', sortable: true },
// 操作列
{ prop: 'action', label: '操作', align: 'center', width: '120', slot: true, fixed: 'right' }
]
const fetchLoading = ref<boolean>(true)
// 分页配置
const paginationAttrs = {
background: true,
hideOnSinglePage: false
}
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 0
})
// 获取数据
const fetchData = () => {
fetchLoading.value = true
// 模拟API调用
console.log('刷新数据...', pagination.value)
setTimeout(() => {
fetchLoading.value = false
pagination.value.total = 100
tableData.value = [
{ id: 1, name: '张三', age: 25, age3: 25, age6: 25, age62: 25, status: 'active' },
{ id: 2, name: '李四', age: 30, status: 'inactive' },
{ id: 3, name: '王五', age: 28, status: 'active' }
]
}, 1000)
}
const handleSelectionChange = (selection: any) => {
console.log('选中行:', selection)
}
// 处理排序变化
const handleSortChange = (sort: any) => {
console.log('排序变化:', sort)
}
onMounted(() => {
fetchData()
})
</script>
更多推荐



所有评论(0)