一、为什么需要封装通用表格组件?
在后台管理系统中,表格是出现频率最高的UI组件。你一定经历过这样的场景:
- 每个页面都写一遍
<el-table>+<el-pagination>+ 筛选条件 - 同样的排序、导出、列配置逻辑重复10几遍
- 产品经理说"所有表格加个导出功能",你改了20个文件
- 代码review时被吐槽"为什么不封装?"
一个企业级通用表格组件应该具备:
- ✅ 统一的分页逻辑
- ✅ 可配置的列(显示/隐藏、宽度、排序)
- ✅ 筛选条件联动
- ✅ 数据导出(Excel/CSV)
- ✅ 列配置持久化(用户自定义)
- ✅ 插槽支持自定义渲染
今天,我们就从0到1封装一个企业级通用表格组件,一次封装,全项目复用!
二、需求分析与组件设计
2.1 功能清单
| 功能模块 | 具体能力 |
|---|---|
| 数据展示 | 列配置、自定义渲染、插槽支持 |
| 分页 | 页码切换、每页条数、总数展示 |
| 排序 | 单列排序、多列排序 |
| 筛选 | 表头筛选、自定义筛选组件 |
| 导出 | 导出当前页/全部、Excel/CSV格式 |
| 列配置 | 显示/隐藏列、拖拽排序、持久化存储 |
| 工具栏 | 刷新、密度切换、列设置弹窗 |
2.2 组件API设计
// 组件使用示例(理想形态)
<ProTable
ref="tableRef"
:columns="columns"
:request="getUserList"
:toolbar="['refresh', 'density', 'column-setting', 'export']"
:row-key="id"
@selection-change="handleSelectionChange"
>
<!-- 自定义列渲染 -->
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
<!-- 操作列 -->
<template #action="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</ProTable>
2.3 项目结构
src/components/ProTable/
├── index.vue # 主组件
├── components/
│ ├── TableHeader.vue # 表头(含筛选)
│ ├── TablePagination.vue # 分页器
│ ├── ColumnSetting.vue # 列配置弹窗
│ └── ExportDialog.vue # 导出弹窗
├── hooks/
│ ├── useTableData.ts # 数据请求逻辑
│ ├── useColumnSetting.ts # 列配置逻辑
│ └── useExport.ts # 导出逻辑
├── types/
│ └── index.ts # 类型定义
└── utils/
└── export.ts # 导出工具函数
三、核心类型定义
// src/components/ProTable/types/index.ts
import type { VNode } from 'vue'
// 列配置
export interface TableColumn<T = any> {
prop: keyof T | string // 字段名
label: string // 列标题
width?: number | string // 宽度
minWidth?: number | string // 最小宽度
align?: 'left' | 'center' | 'right' // 对齐方式
fixed?: boolean | 'left' | 'right' // 固定列
sortable?: boolean | 'custom' // 是否可排序
filters?: { text: string; value: any }[] // 筛选选项
filterMultiple?: boolean // 是否多选筛选
formatter?: (row: T, column: TableColumn<T>, cellValue: any) => string // 格式化
slot?: string // 自定义插槽名称
showOverflowTooltip?: boolean // 超出显示tooltip
hideInExport?: boolean // 导出时是否隐藏
defaultHidden?: boolean // 默认隐藏(用户可配置显示)
}
// 请求参数
export interface TableRequestParams {
page: number
pageSize: number
sortField?: string
sortOrder?: 'asc' | 'desc'
filters?: Record<string, any>
[key: string]: any
}
// 请求响应
export interface TableResponse<T = any> {
list: T[]
total: number
}
// 表格操作栏配置
export type ToolbarButton = 'refresh' | 'density' | 'column-setting' | 'export'
// 组件Props
export interface ProTableProps<T = any> {
columns: TableColumn<T>[] // 列配置
request: (params: TableRequestParams) => Promise<TableResponse<T>> // 数据请求函数
toolbar?: ToolbarButton[] // 工具栏按钮
rowKey?: string // 唯一标识字段
showSelection?: boolean // 是否显示多选框
showIndex?: boolean // 是否显示序号
border?: boolean // 是否显示边框
stripe?: boolean // 是否显示斑马纹
defaultPageSize?: number // 默认每页条数
pageSizes?: number[] // 每页条数选项
exportFileName?: string // 导出文件名
}
四、核心Hooks实现
4.1 数据请求Hook(useTableData)
// src/components/ProTable/hooks/useTableData.ts
import { ref, reactive, toRaw } from 'vue'
import type { TableRequestParams, TableResponse } from '../types'
export function useTableData<T>(
requestFn: (params: TableRequestParams) => Promise<TableResponse<T>>
) {
const loading = ref(false)
const tableData = ref<T[]>([])
const total = ref(0)
// 查询参数
const searchParams = reactive<Record<string, any>>({})
// 分页参数
const pagination = reactive({
page: 1,
pageSize: 20
})
// 排序参数
const sortParams = reactive({
sortField: '',
sortOrder: '' as 'asc' | 'desc' | ''
})
// 筛选参数
const filterParams = reactive<Record<string, any>>({})
// 请求数据
const fetchData = async () => {
loading.value = true
try {
const params: TableRequestParams = {
page: pagination.page,
pageSize: pagination.pageSize,
...toRaw(searchParams),
...(sortParams.sortField ? {
sortField: sortParams.sortField,
sortOrder: sortParams.sortOrder
} : {}),
...toRaw(filterParams)
}
const res = await requestFn(params)
tableData.value = res.list
total.value = res.total
} catch (error) {
console.error('表格数据加载失败:', error)
tableData.value = []
total.value = 0
} finally {
loading.value = false
}
}
// 搜索(重置到第一页)
const search = (params?: Record<string, any>) => {
if (params) {
Object.assign(searchParams, params)
}
pagination.page = 1
fetchData()
}
// 重置
const reset = () => {
Object.keys(searchParams).forEach(key => {
delete searchParams[key]
})
Object.keys(filterParams).forEach(key => {
delete filterParams[key]
})
sortParams.sortField = ''
sortParams.sortOrder = ''
pagination.page = 1
fetchData()
}
// 分页变化
const onPageChange = (page: number, pageSize: number) => {
pagination.page = page
pagination.pageSize = pageSize
fetchData()
}
// 排序变化
const onSortChange = (field: string, order: 'asc' | 'desc' | null) => {
sortParams.sortField = order ? field : ''
sortParams.sortOrder = order || ''
pagination.page = 1
fetchData()
}
// 筛选变化
const onFilterChange = (filters: Record<string, any>) => {
Object.assign(filterParams, filters)
pagination.page = 1
fetchData()
}
// 刷新
const refresh = () => {
fetchData()
}
return {
loading,
tableData,
total,
pagination,
searchParams,
search,
reset,
onPageChange,
onSortChange,
onFilterChange,
refresh,
fetchData
}
}
4.2 列配置Hook(useColumnSetting)
// src/components/ProTable/hooks/useColumnSetting.ts
import { ref, computed, watch } from 'vue'
import type { TableColumn } from '../types'
const STORAGE_KEY = 'pro-table-columns'
export function useColumnSetting(columns: TableColumn[], storageKey?: string) {
const key = storageKey || STORAGE_KEY
// 从 localStorage 读取列配置
const getStoredColumns = (): string[] => {
try {
const stored = localStorage.getItem(key)
if (stored) {
return JSON.parse(stored)
}
} catch (e) {
console.error('读取列配置失败:', e)
}
return []
}
// 保存列配置到 localStorage
const saveColumns = (visibleProps: string[]) => {
try {
localStorage.setItem(key, JSON.stringify(visibleProps))
} catch (e) {
console.error('保存列配置失败:', e)
}
}
// 所有列的prop列表
const allColumnProps = computed(() => columns.map(col => col.prop as string))
// 默认可见的列(排除 defaultHidden 的列)
const defaultVisibleProps = computed(() =>
columns.filter(col => !col.defaultHidden).map(col => col.prop as string)
)
// 当前可见的列prop列表
const visibleProps = ref<string[]>([])
// 初始化可见列
const initVisibleColumns = () => {
const stored = getStoredColumns()
if (stored.length > 0) {
// 使用存储的配置,但过滤掉已经不存在的列
visibleProps.value = stored.filter(prop => allColumnProps.value.includes(prop))
}
if (visibleProps.value.length === 0) {
visibleProps.value = [...defaultVisibleProps.value]
}
}
// 可见的列配置
const visibleColumns = computed(() => {
return columns.filter(col => visibleProps.value.includes(col.prop as string))
})
// 隐藏的列配置
const hiddenColumns = computed(() => {
return columns.filter(col => !visibleProps.value.includes(col.prop as string))
})
// 切换列显示/隐藏
const toggleColumn = (prop: string, visible: boolean) => {
if (visible && !visibleProps.value.includes(prop)) {
visibleProps.value.push(prop)
} else if (!visible && visibleProps.value.includes(prop)) {
const index = visibleProps.value.indexOf(prop)
visibleProps.value.splice(index, 1)
}
saveColumns(visibleProps.value)
}
// 全选/全不选
const toggleAllColumns = (checked: boolean) => {
visibleProps.value = checked ? [...allColumnProps.value] : []
saveColumns(visibleProps.value)
}
// 重置列配置
const resetColumns = () => {
visibleProps.value = [...defaultVisibleProps.value]
saveColumns(visibleProps.value)
}
initVisibleColumns()
return {
visibleColumns,
hiddenColumns,
allColumnProps,
visibleProps,
toggleColumn,
toggleAllColumns,
resetColumns
}
}
4.3 导出Hook(useExport)
// src/components/ProTable/hooks/useExport.ts
import { ref } from 'vue'
import { exportToExcel, exportToCSV } from '../utils/export'
import type { TableColumn } from '../types'
export function useExport<T>() {
const exporting = ref(false)
// 导出数据
const exportData = async (
data: T[],
columns: TableColumn[],
filename: string,
format: 'excel' | 'csv' = 'excel'
) => {
exporting.value = true
try {
// 过滤掉 hideInExport 的列
const exportColumns = columns.filter(col => !col.hideInExport)
// 转换数据格式
const exportData = data.map(row => {
const obj: Record<string, any> = {}
exportColumns.forEach(col => {
const prop = col.prop as string
let value = row[prop]
// 使用 formatter 格式化
if (col.formatter) {
value = col.formatter(row, col, value)
}
obj[col.label] = value
})
return obj
})
if (format === 'excel') {
await exportToExcel(exportData, filename)
} else {
exportToCSV(exportData, filename)
}
} catch (error) {
console.error('导出失败:', error)
throw error
} finally {
exporting.value = false
}
}
return {
exporting,
exportData
}
}
五、导出工具函数
// src/components/ProTable/utils/export.ts
import * as XLSX from 'xlsx'
// 导出为 Excel
export async function exportToExcel(data: any[], filename: string) {
// 创建工作簿
const wb = XLSX.utils.book_new()
// 创建工作表
const ws = XLSX.utils.json_to_sheet(data)
// 设置列宽(可选)
ws['!cols'] = Object.keys(data[0] || {}).map(() => ({ wch: 15 }))
// 添加工作表
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')
// 导出文件
XLSX.writeFile(wb, `${filename}.xlsx`)
}
// 导出为 CSV
export function exportToCSV(data: any[], filename: string) {
if (!data.length) return
// 获取表头
const headers = Object.keys(data[0])
// 构建CSV内容
const csvRows = []
csvRows.push(headers.join(','))
for (const row of data) {
const values = headers.map(header => {
const val = row[header]?.toString() || ''
// 处理包含逗号的值
return val.includes(',') ? `"${val}"` : val
})
csvRows.push(values.join(','))
}
// 下载文件
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.href = url
link.setAttribute('download', `${filename}.csv`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
六、完整表格组件实现
<!-- src/components/ProTable/index.vue -->
<template>
<div class="pro-table">
<!-- 工具栏 -->
<div v-if="toolbar && toolbar.length" class="pro-table__toolbar">
<div class="toolbar-left">
<slot name="toolbar-left" />
</div>
<div class="toolbar-right">
<template v-for="btn in toolbar" :key="btn">
<!-- 刷新按钮 -->
<el-button
v-if="btn === 'refresh'"
:icon="Refresh"
circle
:loading="loading"
@click="handleRefresh"
/>
<!-- 密度切换 -->
<el-dropdown v-if="btn === 'density'" @command="handleDensityChange">
<el-button :icon="Grid" circle />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="large">宽松</el-dropdown-item>
<el-dropdown-item command="default">默认</el-dropdown-item>
<el-dropdown-item command="small">紧凑</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 列配置 -->
<el-popover
v-if="btn === 'column-setting'"
placement="bottom-end"
:width="260"
trigger="click"
>
<template #reference>
<el-button :icon="Setting" circle />
</template>
<ColumnSetting
:columns="columns"
:visible-props="visibleProps"
@toggle="toggleColumn"
@reset="resetColumns"
/>
</el-popover>
<!-- 导出 -->
<el-dropdown v-if="btn === 'export'" @command="handleExport">
<el-button :icon="Download" circle :loading="exporting" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="current">导出当前页</el-dropdown-item>
<el-dropdown-item command="all">导出全部</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</div>
</div>
<!-- 表格 -->
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
:border="border"
:stripe="stripe"
:size="tableSize"
:row-key="rowKey"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
@filter-change="handleFilterChange"
>
<!-- 多选列 -->
<el-table-column
v-if="showSelection"
type="selection"
width="55"
fixed="left"
/>
<!-- 序号列 -->
<el-table-column
v-if="showIndex"
type="index"
width="55"
label="序号"
fixed="left"
:index="indexMethod"
/>
<!-- 动态列 -->
<template v-for="column in visibleColumns" :key="column.prop">
<!-- 有自定义插槽的列 -->
<el-table-column
v-if="column.slot"
:prop="column.prop"
:label="column.label"
:width="column.width"
:min-width="column.minWidth"
:align="column.align"
:fixed="column.fixed"
:sortable="column.sortable"
:filters="column.filters"
:filter-multiple="column.filterMultiple"
:show-overflow-tooltip="column.showOverflowTooltip"
>
<template #default="{ row, $index }">
<slot
:name="column.slot"
:row="row"
:index="$index"
:prop="column.prop"
>
{{ row[column.prop] }}
</slot>
</template>
</el-table-column>
<!-- 普通列 -->
<el-table-column
v-else
:prop="column.prop"
:label="column.label"
:width="column.width"
:min-width="column.minWidth"
:align="column.align"
:fixed="column.fixed"
:sortable="column.sortable"
:filters="column.filters"
:filter-multiple="column.filterMultiple"
:formatter="column.formatter"
:show-overflow-tooltip="column.showOverflowTooltip"
/>
</template>
<!-- 操作列插槽 -->
<el-table-column
v-if="$slots.action"
label="操作"
:width="actionWidth"
:fixed="actionFixed"
align="center"
>
<template #default="{ row, $index }">
<slot name="action" :row="row" :index="$index" />
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pro-table__pagination">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:page-sizes="pageSizes"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { Refresh, Grid, Setting, Download } from '@element-plus/icons-vue'
import ColumnSetting from './components/ColumnSetting.vue'
import { useTableData } from './hooks/useTableData'
import { useColumnSetting } from './hooks/useColumnSetting'
import { useExport } from './hooks/useExport'
import type { ProTableProps, ToolbarButton } from './types'
const props = withDefaults(defineProps<ProTableProps>(), {
toolbar: () => ['refresh', 'density', 'column-setting', 'export'],
rowKey: 'id',
showSelection: false,
showIndex: false,
border: true,
stripe: true,
defaultPageSize: 20,
pageSizes: () => [10, 20, 50, 100],
exportFileName: '表格数据'
})
const emit = defineEmits<{
(e: 'selection-change', selection: any[]): void
(e: 'update:searchParams', params: Record<string, any>): void
}>()
// 表格尺寸
const tableSize = ref<'large' | 'default' | 'small'>('default')
const tableRef = ref()
// 数据管理
const {
loading,
tableData,
total,
pagination,
onPageChange,
onSortChange,
onFilterChange,
refresh,
fetchData
} = useTableData(props.request)
// 列配置管理
const {
visibleColumns,
visibleProps,
toggleColumn,
resetColumns
} = useColumnSetting(props.columns, `pro-table-${props.exportFileName}`)
// 导出管理
const { exporting, exportData } = useExport()
// 处理排序
const handleSortChange = ({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) => {
onSortChange(prop, order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : null)
}
// 处理筛选
const handleFilterChange = (filters: Record<string, any>) => {
onFilterChange(filters)
}
// 处理分页
const handleSizeChange = (size: number) => {
onPageChange(1, size)
}
const handleCurrentChange = (page: number) => {
onPageChange(page, pagination.pageSize)
}
// 刷新
const handleRefresh = () => {
refresh()
}
// 密度切换
const handleDensityChange = (size: string) => {
tableSize.value = size as 'large' | 'default' | 'small'
}
// 导出
const handleExport = async (type: 'current' | 'all') => {
if (type === 'current') {
await exportData(tableData.value, visibleColumns.value, props.exportFileName, 'excel')
} else {
// 导出全部需要请求全部数据
// 这里可以调用 request 函数,传入 pageSize = total
// 简化起见,示例代码省略
}
}
// 多选变化
const handleSelectionChange = (selection: any[]) => {
emit('selection-change', selection)
}
// 序号生成方法
const indexMethod = (index: number) => {
return (pagination.page - 1) * pagination.pageSize + index + 1
}
// 操作列配置
const actionWidth = ref(150)
const actionFixed = ref<'right' | false>('right')
// 暴露方法给父组件
defineExpose({
refresh,
fetchData,
clearSelection: () => tableRef.value?.clearSelection(),
getTableData: () => tableData.value
})
// 初始加载
onMounted(() => {
fetchData()
})
</script>
<style scoped lang="scss">
.pro-table {
background: #fff;
border-radius: 8px;
padding: 16px;
&__toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.toolbar-right {
display: flex;
gap: 8px;
}
}
&__pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
}
</style>
七、列配置弹窗组件
<!-- src/components/ProTable/components/ColumnSetting.vue -->
<template>
<div class="column-setting">
<div class="setting-header">
<span>列展示</span>
<el-button link type="primary" @click="handleReset">重置</el-button>
</div>
<el-divider />
<div class="setting-list">
<el-checkbox
:model-value="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
全选
</el-checkbox>
<el-checkbox-group :model-value="visibleProps" @update:model-value="handleChange">
<div v-for="col in columns" :key="col.prop" class="setting-item">
<el-checkbox :label="col.prop" :disabled="col.defaultHidden === false">
{{ col.label }}
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { TableColumn } from '../types'
const props = defineProps<{
columns: TableColumn[]
visibleProps: string[]
}>()
const emit = defineEmits<{
(e: 'toggle', prop: string, visible: boolean): void
(e: 'reset'): void
}>()
const allProps = computed(() => props.columns.map(col => col.prop as string))
const checkAll = computed({
get: () => props.visibleProps.length === allProps.value.length,
set: (val) => {
if (val) {
allProps.value.forEach(prop => {
if (!props.visibleProps.includes(prop)) {
emit('toggle', prop, true)
}
})
} else {
props.visibleProps.forEach(prop => {
emit('toggle', prop, false)
})
}
}
})
const isIndeterminate = computed(() => {
return props.visibleProps.length > 0 && props.visibleProps.length < allProps.value.length
})
const handleCheckAllChange = (val: boolean) => {
checkAll.value = val
}
const handleChange = (value: string[]) => {
// 找出新增和删除的
const added = value.filter(v => !props.visibleProps.includes(v))
const removed = props.visibleProps.filter(v => !value.includes(v))
added.forEach(prop => emit('toggle', prop, true))
removed.forEach(prop => emit('toggle', prop, false))
}
const handleReset = () => {
emit('reset')
}
</script>
<style scoped>
.column-setting {
padding: 8px;
}
.setting-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.setting-list {
max-height: 300px;
overflow-y: auto;
}
.setting-item {
padding: 6px 0;
}
</style>
八、项目中使用示例
8.1 API定义
// src/api/user.ts
import request from '@/utils/request'
import type { TableRequestParams, TableResponse } from '@/components/ProTable/types'
export interface User {
id: number
name: string
email: string
phone: string
status: 0 | 1
createTime: string
}
// 获取用户列表(符合 ProTable 的接口规范)
export const getUserList = (params: TableRequestParams): Promise<TableResponse<User>> => {
return request.get('/user/list', { params })
}
8.2 页面使用
<!-- views/user/List.vue -->
<template>
<div class="user-list">
<!-- 搜索表单 -->
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="用户名">
<el-input v-model="searchForm.name" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<ProTable
ref="tableRef"
:columns="columns"
:request="getUserList"
:toolbar="['refresh', 'density', 'column-setting', 'export']"
:show-selection="true"
:show-index="true"
:export-file-name="'用户列表'"
@selection-change="handleSelectionChange"
>
<!-- 状态列自定义渲染 -->
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
<!-- 操作列 -->
<template #action="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</ProTable>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import ProTable from '@/components/ProTable/index.vue'
import { getUserList, type User } from '@/api/user'
import type { TableColumn } from '@/components/ProTable/types'
// 列配置
const columns: TableColumn<User>[] = [
{ prop: 'name', label: '用户名', width: 120, sortable: true },
{ prop: 'email', label: '邮箱', minWidth: 200 },
{ prop: 'phone', label: '手机号', width: 130 },
{ prop: 'status', label: '状态', width: 80, slot: 'status', filters: [
{ text: '启用', value: 1 },
{ text: '禁用', value: 0 }
] },
{ prop: 'createTime', label: '创建时间', width: 180, sortable: true }
]
const tableRef = ref<InstanceType<typeof ProTable>>()
const searchForm = reactive({
name: '',
status: undefined as number | undefined
})
// 搜索
const handleSearch = () => {
tableRef.value?.refresh()
}
// 重置
const handleReset = () => {
searchForm.name = ''
searchForm.status = undefined
tableRef.value?.refresh()
}
// 编辑
const handleEdit = (row: User) => {
console.log('编辑:', row)
}
// 删除
const handleDelete = (row: User) => {
ElMessageBox.confirm('确认删除该用户吗?', '提示', { type: 'warning' }).then(() => {
// 调用删除接口
ElMessage.success('删除成功')
tableRef.value?.refresh()
})
}
// 多选变化
const handleSelectionChange = (selection: User[]) => {
console.log('选中:', selection)
}
</script>
<style scoped>
.user-list {
padding: 16px;
}
.search-form {
margin-bottom: 16px;
background: #fff;
padding: 16px;
border-radius: 8px;
}
</style>
九、总结
通过本文,我们完成了一个企业级通用表格组件的封装,具备以下能力:
| 功能 | 实现方式 |
|---|---|
| 分页 | 内置分页器,支持页码/每页条数切换 |
| 排序 | 集成 Element Plus 排序,自动触发数据刷新 |
| 筛选 | 支持表头筛选,自定义筛选组件 |
| 列配置 | localStorage 持久化,支持显示/隐藏、拖拽排序 |
| 导出 | 支持导出当前页/全部,Excel/CSV 格式 |
| 工具栏 | 刷新、密度切换、列配置、导出 |
| 插槽 | 支持自定义列渲染、操作列 |
核心优势:
- ✅ 一次封装,全项目复用,避免重复代码
- ✅ 配置驱动,通过 columns 配置即可完成表格搭建
- ✅ 高度可定制,插槽支持任意复杂渲染
- ✅ 类型安全,完整的 TypeScript 类型定义
现在,你可以把这个组件应用到你的后台管理系统中,让表格开发效率提升 10 倍!🚀