CURD页面组件封装——数据表格组件

97 阅读7分钟

前言

对于面向企业的项目而言,前端开发中最常见的场景之一便是后台管理系统,而这类系统的核心页面多为基于表格表单的 CURD(增删改查)操作页面。随着同类页面开发量的增加,封装一套通用的 CURD 功能组件变得尤为必要——这不仅能显著提升开发效率,避免重复性工作,更能为后续的统一修改与维护提供便利。

一个标准的 CURD 页面通常包含以下核心功能模块:搜索条、操作工具条、数据表格、新增 / 编辑弹窗、详情弹窗等。

本方案将采用 “分而治之” 的策略:将每个功能模块封装为独立的基础组件,再通过组件组合的方式快速搭建完整的 CURD 页面。

在之前的文章中已经介绍了搜索条功能模块组件的封装,参见文章:

CURD页面组件封装——搜索组件

本篇文章将继续介绍数据表格功能模块的封装实现细节。欢迎大家持续关注,并提出宝贵的建议与反馈。

数据表格

数据表格主要由表格主体和分页器两部分组成,二者配合实现结构化数据的高效展示与交互。

表格主体采用 JSON 配置化设计,与搜索条组件逻辑一致:通过配置项数组动态渲染表格列,无需重复编写列标签,降低冗余且便于修改。

若表格列需特殊展示(如标签、图片、操作按钮),可通过配置slot插槽实现,即可自定义单元格内容。

实现

template模板

1.动态列渲染:用配置驱动视图

表格最核心的功能是列渲染,传统写法中需要手动编写多个<el-table-column>,而封装的关键就在于用配置数组动态生成列。模板中通过v-for="item in columns"遍历配置数组,将每个配置项映射为<el-table-column>

<template v-for="item in columns" :key="item.prop">
  <el-table-column
    :type="item.type"
    :prop="item.prop"
    :label="item.label"
    :width="item.width"
    :min-width="item.minWidth"
    :align="item.align"
    :sortable="item.sortable"
    :class-name="item.className"
    :formatter="item.formatter"
  />
</template>

这里的设计思路是保留原生表格列的核心属性,同时通过配置项传递。比如:

  • proplabel对应数据字段和列标题,是表格的基础配置
  • width/minWidth控制列宽,align设置对齐方式(left/center/right)
  • sortable支持排序功能
  • formatter可直接配置单元格格式化函数

这种设计既降低了学习成本(和原生表格属性一致),又实现了 "一处配置,多处复用"。

2.特殊列的内置支持

除了动态列,业务中常需要复选框列(selection)和序号列(index),模板通过条件渲染实现:

<el-table-column v-if="showSelection" type="selection" width="40" /> 
<el-table-column v-if="showIndex" type="index" label="序号" width="60" />

通过showSelectionshowIndex两个布尔值参数,调用者可以一键开启 / 关闭这些特殊列,无需重复编写代码。

3.通过插槽自定义单元格

固定的配置项无法满足所有场景,比如需要在单元格中显示按钮组、图片或自定义组件。这时插槽(slot) 就成了关键的扩展点。

  1. 单元格内容插槽

模板中为每个列提供了内容插槽,通过配置项的slot字段启用:

<template #default="{ row }">
  <!-- 列内容插槽 -->
  <slot v-if="item.slot" :name="item.slot" :row="row"></slot>
  <span v-else>{{ row[item.prop] }}</span>
</template>
  • 当配置item.slot = 'operation'时,会渲染名为operation的插槽,并传入当前行数据row,可用于添加表格的操作列
  • 未配置插槽时,默认显示row[item.prop],即数据字段的原始值
<!-- 调用组件时 -->
<ts-table :columns="columns">
  <template #operation="{ row }">
    <el-button @click="edit(row)">编辑</el-button>
    <el-button @click="delete(row)">删除</el-button>
  </template>
</ts-table>

<script>
  columns = [
    // 其他列...
    { label: '操作', slot: 'operation' }
  ]
</script>
  1. 表头插槽

除了单元格内容,表头有时也需要自定义(比如添加表头说明),模板通过headerSlot字段支持表头插槽:

<template v-if="item.headerSlot" #header>
  <slot :name="item.headerSlot"></slot>
</template>

4.原生属性透传

v-bind="$attrs"外部属性绑定在<el-table>标签上,调用者可以直接使用 Element Plus 表格的所有原生属性(如borderstripe等),无需在封装组件中重复声明这些属性,减少维护成本。

<el-table ref="tableRef" v-bind="$attrs" v-loading="loading" :max-height="height">

例如调用时添加边框和斑马纹:

<ts-table border stripe :columns="columns" :data="tableData" />

5.动态高度适配

表格高度是一个常见的痛点。我们希望单应用页面不会出现滚动条,如果表格内容超出一屏,通过表格内形成滚动条向下浏览。

我们使用 Element Plus 的<el-auto-resizer>组件解决这个问题。el-auto-resizer会自动计算父容器的剩余空间高度,并通过插槽传递给表格。

<el-auto-resizer>
  <template #default="{ height }">
    <el-table :max-height="height" />
  </template>
</el-auto-resizer>

6.分页器

分页是表格的常见需求,但并非所有场景都需要。模板将分页器设计为可选模块:

<div v-if="showPage" class="page-wrapper">
  <el-pagination
    :current-page="currentPage"
    :page-size="pageSize"
    :page-sizes="pageSizes"
    :total="total"
    :layout="pageLayout"
    background
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
  />
</div>
  • 通过showPage参数控制是否显示分页器
  • 暴露currentPagepageSize等分页相关参数
  • 监听size-changecurrent-change事件,将分页变化传递给父组件

完整代码

<template>
  <div class="ts-table">
    <div class="table-wrapper">
      <el-auto-resizer>
        <template #default="{ height }">
          <el-table ref="tableRef" v-bind="$attrs" v-loading="loading" :max-height="height">
            <el-table-column v-if="showSelection" type="selection" width="40" />
            <el-table-column v-if="showIndex" type="index" label="序号" width="60" />
            <template v-for="item in columns" :key="item.prop">
              <el-table-column
                :type="item.type"
                :prop="item.prop"
                :label="item.label"
                :width="item.width"
                :min-width="item.minWidth"
                :align="item.align"
                :sortable="item.sortable"
                :class-name="item.className"
                :formatter="item.formatter"
              >
                <template #default="{ row }">
                  <!-- 列内容插槽-->
                  <slot v-if="item.slot" :name="item.slot" :row="row"></slot>
                  <span v-else>{{ row[item.prop] }}</span>
                </template>
                <!-- 列表头插槽-->
                <template v-if="item.headerSlot" #header>
                  <slot :name="item.headerSlot"></slot>
                </template>
              </el-table-column>
            </template>
          </el-table>
        </template>
      </el-auto-resizer>
    </div>
    <!-- 分页器 -->
    <div v-if="showPage" class="page-wrapper">
      <el-pagination
        :current-page="currentPage"
        :page-size="pageSize"
        :page-sizes="pageSizes"
        :total="total"
        :layout="pageLayout"
        background
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>

<style lang="scss" scoped>
.page-base {
  display: flex;
  flex-direction: column;
  height: 100%;
  .tool-wrapper {
    margin-bottom: 10px;
  }
  .table-wrapper {
    flex: 1;
    .el-link + .el-link {
      margin-left: 10px;
    }
  }
}
</style>

script逻辑

组件对外暴露多组 props 配置项:

  • 表格基础配置:必填项columns(列配置数组),可选showIndex(默认显序号列)、showSelection(默认隐复选列)、loading(加载态);

  • 分页控制配置:showPage(默认显分页器)、total(数据总条数)、currentPage(当前页)、pageSize(每页条数)、pageSizes(可选条数)、pageLayout(布局)。

对外抛出两个事件:sizeChange(每页条数变化回调)、currentChange(页码变化回调),支持外部自定义数据加载逻辑。

对外暴露一个方法:getSelection(获取表格选中行数据),同时通过inheritAttrs: false配合模板实现属性精准透传。

<script setup>
const props = defineProps({
  columns: {
    type: Array,
    required: true,
    default: () => []
  },
  showIndex: {
    type: Boolean,
    default: true
  },
  showSelection: {
    type: Boolean,
    default: false
  },
  loading: {
    type: Boolean,
    default: false
  },
  showPage: {
    type: Boolean,
    default: true
  },
  // 总条数
  total: {
    type: Number,
    default: 0
  },
  // 当前页码
  currentPage: {
    type: Number,
    default: 10
  },
  // 默认每页条数
  pageSize: {
    type: Number,
    default: 10
  },
  // 可选的每页条数
  pageSizes: {
    type: Array,
    default: () => [10, 20, 50, 100]
  },
  // 分页器布局
  pageLayout: {
    type: String,
    default: 'total, sizes, prev, pager, next, jumper'
  }
});
const emit = defineEmits(['sizeChange', 'currentChange']);
defineOptions({
  inheritAttrs: false
});
const tableRef = ref();

function handleSizeChange(val) {
  emit('sizeChange', val);
}
function handleCurrentChange(val) {
  emit('currentChange', val);
}
function getSelection() {
  return tableRef.value.getSelectionRows();
}

defineExpose({
  getSelection
});
</script>

调用

以下为组件的完整调用示例,包含基础配置项与插槽用法。

tableData为通过getData调用接口获取到的表格数据,tableColumns为json表格列配置项数组。

其中“通讯地址”列使用了内容插槽和表头插槽,并且添加了operation操作列插槽可以插入操作按钮。

<template>
<div class="table-wrapper">
  <ts-table
	ref="tsTableRef"
	:loading="loading"
	:data="tableData"
	:columns="tableColumns"
	stripe
	:row-class-name="tableRowClassName"
	show-overflow-tooltip
	style="width: 100%"
	show-selection
	:span-method="arraySpanMethod"
	:current-page="pageOption.pageNum"
	:page-size="pageOption.pageSize"
	:total="total"
	@row-click="handleRowClick"
	@size-change="handleSizeChange"
	@current-change="handleCurrentChange"
  >
	<!-- 展开列插槽 -->
	<template #expand="{ row }">
	  {{ row }}
	</template>
	<!-- 表格列插槽 -->
	<template #address="{ row }">
	  <span>slot: {{ row.address }}</span>
	</template>
	<!-- 自定义表头插槽 -->
	<template #header-address>
	  <el-tooltip content="这里是自定义表头的说明" placement="top">
		<div>自定义地址表头</div>
	  </el-tooltip>
	</template>
	<template #operation="{ row }">
	  <el-link type="primary" @click.stop="handleEdit(row)">编辑</el-link>
	  <el-link type="primary" @click.stop="handleDel(row)">删除</el-link>
	  <el-link type="primary" @click.stop="handleDetail(row)">详情</el-link>
	</template>
  </ts-table>
</div>
</template>

<script setup>
const tsTableRef = ref();
const tableData = ref([]);
const tableColumns = reactive([
  {
    type: 'expand',
    slot: 'expand'
  },
  {
    prop: 'name',
    label: '姓名',
    align: 'center'
  },
  {
    prop: 'gender',
    label: '性别',
    width: 120
  },
  {
    prop: 'age',
    label: '年龄',
    sortable: true,
    width: 120
  },
  {
    prop: 'date',
    label: '出生日期',
    fixed: true
  },
  {
    prop: 'address',
    label: '通讯地址',
    slot: 'address',
    headerSlot: 'header-address',
    width: 600
  },
  {
    label: '操作',
    slot: 'operation'
  }
]);
const pageOption = reactive({
  pageNum: 1,
  pageSize: 10
});
const total = ref(0);
const loading = ref(false);

watch(pageOption, getData);

const tableRowClassName = ({ rowIndex }) => {
  if (rowIndex === 0) {
    return 'warning-row';
  } else if (rowIndex === 2) {
    return 'success-row';
  }
  return '';
};

/**
 * @description: 分页器每页数据条数切换
 * @param {*} val
 * @return {*}
 */
function handleSizeChange(val) {
  pageOption.pageSize = val;
}
/**
 * @description: 分页器当前页码切换
 * @param {*} val
 * @return {*}
 */
function handleCurrentChange(val) {
  pageOption.pageNum = val;
}

/**
 * @description: 获取表格数据
 * @return {*}
 */
async function getData() {
  try {
    loading.value = true;
    const data = { ...pageOption, ...handleSearchData(searchData, searchColumns) };
    console.log('getData data:  ', data);
    const res = await getItems(data);
    console.log('getData res:  ', res);
    tableData.value = res.rows;
    total.value = res.total;
  } catch (err) {
    console.error(err);
  } finally {
    loading.value = false;
  }
}

getData();
</script>

总结

数据表格组件的核心设计思路是 “配置化 + 插槽扩展”:通过 JSON 配置数组实现表格列的动态渲染;借助插槽机制支持单元格、表头、特殊列的自定义,兼顾业务灵活性;搭配分页器参数配置覆盖数据展示与分页控制的通用场景。

后续将继续介绍新增 / 编辑弹窗、详情弹窗等组件的封装实现,敬请关注!