前言 👋
在前一篇文章中,我们封装了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 :data="list" v-loading="loading"> <el-table-column prop="name" label="姓名"></el-table-column> <!-- 一堆列... --> </el-table> <el-pagination background layout="total, sizes, prev, pager, next" :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </div> </template> <script> export default { data() { /* 大量样板代码 */ }, methods: { /* 增删改查方法 */ } } </script>使用后(我们的方案):
<template> <ProTable ref="tableRef" :columns="columns" :request="fetchTableData" :search-form="queryParams" > <!-- 操作列插槽 --> <template #operation="{ row }"> <el-button @click="crud.edit(row)">编辑</el-button> <el-button @click="crud.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 class="pro-table"> <!-- 表格主体 --> <el-table ref="elTableRef" v-loading="loading" :data="tableData" v-bind="$attrs" height="100%" > <!-- 序号列 --> <el-table-column v-if="showIndex" type="index" label="序号" width="60" align="center" /> <!-- 动态列 --> <template v-for="(col, index) in columns"> <!-- 普通列 --> <el-table-column v-if="!col.children && col.prop !== 'operation'" :key="index" v-bind="col" > <!-- 自定义列的插槽 --> <template slot-scope="{ row, $index }"> <slot v-if="col.slot" :name="col.prop" :row="row" :index="$index" /> <span v-else>{{ row[col.prop] }}</span> </template> </el-table-column> <!-- 操作列 --> <el-table-column v-if="col.prop === 'operation'" :key="index" v-bind="col" > <template slot-scope="{ row, $index }"> <slot name="operation" :row="row" :index="$index" /> </template> </el-table-column> </template> </el-table> <!-- 分页 --> <div class="pagination-container" v-if="showPagination"> <el-pagination background layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" :total="total" :page-size.sync="pagination.pageSize" :current-page.sync="pagination.pageNum" @size-change="handleSizeChange" @current-change="handlePageChange" /> </div> </div> </template>2. 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 class="page-container"> <!-- 搜索区 --> <SearchForm :config="searchConfig" :model.sync="queryParams" @search="handleSearch"> <el-button type="primary" @click="crud.openAddDialog()">新增</el-button> </SearchForm> <!-- 表格区 --> <ProTable ref="tableRef" :columns="tableColumns" :request="fetchTableData" :search-form="queryParams" > <template #status="{ row }"> <el-tag :type="row.status === 1 ? 'success' : 'danger'"> {{ row.status === 1 ? '启用' : '禁用' }} </el-tag> </template> <template #operation="{ row }"> <el-button type="text" @click="crud.openEditDialog(row)">编辑</el-button> <el-button type="text" class="danger-text" @click="crud.del(row.id)">删除</el-button> </template> </ProTable> <!-- 新增/编辑弹窗 (示例) --> <el-dialog :title="crudState.dialogTitle" :visible.sync="crudState.dialogVisible"> <el-form :model="crudState.form" label-width="80px"> <el-form-item label="用户名"> <el-input v-model="crudState.form.username"></el-input> </el-form-item> </el-form> <span slot="footer"> <el-button @click="crudState.dialogVisible = false">取消</el-button> <el-button type="primary" @click="crud.submit(submitApi)">确定</el-button> </span> </el-dialog> </div> </template>2. 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,不用逐个页面查找。