前言 在前一篇文章中我们封装了SearchForm组件解决了列表页的头部搜索问题。今天我们要攻克中后台最核心、最高频出现的组件——Table表格。你是否厌倦了在每个.vue文件中重复写这样的代码el-table/el-table-columnloading状态分页组件el-pagination新增/编辑/删除/查看的逻辑本文将带你封装一个企业级 Table 组件并引入CRUD Hooks​ 的设计思想让你的列表页代码量减少80%一、设计目标与最终效果 我们要实现什么ProTable组件集成了表格、分页、loading、空状态。Column 配置化像SearchForm一样通过配置生成列。CRUD Hooks通过useCrud函数一键生成列表/新增/编辑/删除逻辑。使用前 vs 使用后使用前传统写法template div el-table :datalist v-loadingloading el-table-column propname label姓名/el-table-column !-- 一堆列... -- /el-table el-pagination background layouttotal, sizes, prev, pager, next :totaltotal size-changehandleSizeChange current-changehandleCurrentChange / /div /template script export default { data() { /* 大量样板代码 */ }, methods: { /* 增删改查方法 */ } } /script使用后我们的方案template ProTable reftableRef :columnscolumns :requestfetchTableData :search-formqueryParams !-- 操作列插槽 -- template #operation{ row } el-button clickcrud.edit(row)编辑/el-button el-button clickcrud.del(row.id)删除/el-button /template /ProTable /template script import { useCrud } from /hooks/useCrud export default { setup() { const { crud, fetchTableData } useCrud(/api/user) return { crud, fetchTableData } } } /script(注Vue2 中 setup 需借助vue/composition-api但我们先用 Options API 实现核心逻辑)二、核心实现ProTable 组件 ️我们先来实现ProTable.vue。1. 组件结构 (src/components/ProTable/index.vue)template div classpro-table !-- 表格主体 -- el-table refelTableRef v-loadingloading :datatableData v-bind$attrs height100% !-- 序号列 -- el-table-column v-ifshowIndex typeindex label序号 width60 aligncenter / !-- 动态列 -- template v-for(col, index) in columns !-- 普通列 -- el-table-column v-if!col.children col.prop ! operation :keyindex v-bindcol !-- 自定义列的插槽 -- template slot-scope{ row, $index } slot v-ifcol.slot :namecol.prop :rowrow :index$index / span v-else{{ row[col.prop] }}/span /template /el-table-column !-- 操作列 -- el-table-column v-ifcol.prop operation :keyindex v-bindcol template slot-scope{ row, $index } slot nameoperation :rowrow :index$index / /template /el-table-column /template /el-table !-- 分页 -- div classpagination-container v-ifshowPagination el-pagination background layouttotal, sizes, prev, pager, next, jumper :page-sizes[10, 20, 50, 100] :totaltotal :page-size.syncpagination.pageSize :current-page.syncpagination.pageNum size-changehandleSizeChange current-changehandlePageChange / /div /div /template2. JS 逻辑script export default { name: ProTable, props: { columns: { type: Array, required: true }, request: { type: Function, required: true }, // 请求API的函数 searchForm: { type: Object, default: () ({}) }, showIndex: { type: Boolean, default: true }, showPagination: { type: Boolean, default: true } }, data() { return { loading: false, tableData: [], total: 0, pagination: { pageNum: 1, pageSize: 10 } } }, watch: { // 监听搜索条件变化自动刷新表格 searchForm: { deep: true, handler() { this.pagination.pageNum 1 // 搜索时重置页码 this.refresh() } } }, mounted() { this.refresh() }, methods: { // 刷新表格 async refresh() { this.loading true try { const params { ...this.pagination, ...this.searchForm } const res await this.request(params) this.tableData res.list || [] this.total res.total || 0 } catch (error) { console.error(error) } finally { this.loading false } }, handleSizeChange(size) { this.pagination.pageSize size this.refresh() }, handlePageChange(page) { this.pagination.pageNum page this.refresh() } } } /script三、CRUD Hooks 设计 (useCrud.js) 虽然 Vue2 原生不支持 Composition API但我们可以模拟一个Hooks 工厂函数将 CRUD 逻辑抽离。1. 创建 Hook (src/hooks/useCrud.js)import { MessageBox } from element-ui import request from /utils/request export function useCrud(apiPrefix) { const state { dialogVisible: false, dialogTitle: , form: {}, rules: {}, isEdit: false } const methods { // 打开新增弹窗 openAddDialog(defaultForm {}) { state.dialogTitle 新增 state.isEdit false state.form { ...defaultForm } state.dialogVisible true }, // 打开编辑弹窗 openEditDialog(row) { state.dialogTitle 编辑 state.isEdit true state.form { ...row } // 深拷贝视情况而定 state.dialogVisible true }, // 提交表单 async submit(api) { try { const res await api(state.form) if (res.code 20000) { this.$message.success(${state.dialogTitle}成功) state.dialogVisible false // 通知表格刷新 this.$refs.tableRef?.refresh() } } catch (e) { console.error(e) } }, // 删除 async del(id) { try { await MessageBox.confirm(此操作将永久删除该数据, 是否继续?, 提示, { type: warning }) const res await request({ url: ${apiPrefix}/delete/${id}, method: post }) if (res.code 20000) { this.$message.success(删除成功) this.$refs.tableRef?.refresh() } } catch (e) { if (e ! cancel) console.error(e) } } } return { crudState: state, crudMethods: methods } }四、页面集成与使用 现在我们把SearchForm,ProTable,useCrud组合起来。1. 页面视图 (views/user/index.vue)template div classpage-container !-- 搜索区 -- SearchForm :configsearchConfig :model.syncqueryParams searchhandleSearch el-button typeprimary clickcrud.openAddDialog()新增/el-button /SearchForm !-- 表格区 -- ProTable reftableRef :columnstableColumns :requestfetchTableData :search-formqueryParams template #status{ row } el-tag :typerow.status 1 ? success : danger {{ row.status 1 ? 启用 : 禁用 }} /el-tag /template template #operation{ row } el-button typetext clickcrud.openEditDialog(row)编辑/el-button el-button typetext classdanger-text clickcrud.del(row.id)删除/el-button /template /ProTable !-- 新增/编辑弹窗 (示例) -- el-dialog :titlecrudState.dialogTitle :visible.synccrudState.dialogVisible el-form :modelcrudState.form label-width80px el-form-item label用户名 el-input v-modelcrudState.form.username/el-input /el-form-item /el-form span slotfooter el-button clickcrudState.dialogVisible false取消/el-button el-button typeprimary clickcrud.submit(submitApi)确定/el-button /span /el-dialog /div /template2. JS 逻辑script import { useCrud } from /hooks/useCrud import SearchForm from /components/SearchForm import ProTable from /components/ProTable export default { components: { SearchForm, ProTable }, data() { return { queryParams: { username: }, searchConfig: [{ label: 用户名, prop: username, type: input }], tableColumns: [ { prop: username, label: 用户名 }, { prop: email, label: 邮箱 }, { prop: status, label: 状态, slot: true }, { prop: operation, label: 操作, fixed: right, width: 150 } ] } }, created() { const { crudState, crudMethods } useCrud(/api/user) this.crudState crudState this.crud crudMethods.bind(this) // 绑定 this }, methods: { fetchTableData(params) { return request({ url: /api/user/list, params }) }, submitApi(form) { const api this.crudState.isEdit ? /api/user/update : /api/user/add return request({ url: api, method: post, data: form }) } } } /script五、总结与思考 至此我们已经搭建了一个非常强大的中后台开发模式SearchForm负责输入。ProTable负责展示与分页。useCrud Hook负责业务逻辑增删改查。这种模式的优势✅极高复用性所有列表页几乎一模一样。✅关注点分离页面只关心配置和数据流不关心具体实现。✅易于维护修改表格逻辑只需改ProTable不用逐个页面查找。