Vue3 + Element Plus 二次封装 Table 组件实战指南

1,218 阅读3分钟

前言

在前端开发中,表格(Table)组件是最常用的UI组件之一。Element Plus作为Vue3的流行UI库,提供了功能强大的Table组件。但在实际项目中,直接使用原生Table组件往往会导致代码重复、维护困难。本文将介绍如何基于Vue3和Element Plus二次封装一个更高效、更易用的Table组件。

为什么需要二次封装Table组件?

  • 统一风格:保持项目中所有表格样式和行为一致

  • 减少重复代码:封装公共逻辑,避免每个页面重复编写相似代码

  • 提高开发效率:通过配置化方式快速生成表格

  • 便于维护:统一修改点,一处修改全局生效

基础封装实现

1、创建基础组件

<template>
  <el-table
    v-bind="$attrs"
    :data="tableData"
    :border="border"
    :stripe="stripe"
    @selection-change="handleSelectionChange"
  >
    <template v-for="item in columns" :key="item.prop">
      <!-- 选择列 -->
      <el-table-column v-if="item.type === 'selection'" type="selection" width="55" />
      
      <!-- 序号列 -->
      <el-table-column 
        v-else-if="item.type === 'index'" 
        type="index" 
        :width="item.width || '80'" 
        :label="item.label || '序号'" 
      />
      
      <!-- 普通列 -->
      <el-table-column
        v-else
        :prop="item.prop"
        :label="item.label"
        :width="item.width"
        :min-width="item.minWidth"
        :align="item.align || 'center'"
      >
        <!-- 自定义列内容 -->
        <template #default="scope" v-if="item.slot">
          <slot :name="item.slot" :row="scope.row"></slot>
        </template>
      </el-table-column>
    </template>
  </el-table>
</template>

<script setup>
const props = defineProps({
  columns: {
    type: Array,
    required: true
  },
  tableData: {
    type: Array,
    required: true
  },
  border: {
    type: Boolean,
    default: true
  },
  stripe: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits(['selection-change'])

const handleSelectionChange = (val) => {
  emit('selection-change', val)
}
</script>

2、使用示例

<template>
  <ProTable
    :columns="columns"
    :table-data="tableData"
    @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 size="small" @click="handleEdit(row)">编辑</el-button>
      <el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
    </template>
  </ProTable>
</template>

<script setup>
const columns = [
  { type: 'selection' },
  { type: 'index', label: '序号' },
  { prop: 'name', label: '姓名', width: '120' },
  { prop: 'age', label: '年龄', width: '80' },
  { prop: 'address', label: '地址', minWidth: '200' },
  { prop: 'status', label: '状态', slot: 'status' },
  { prop: 'action', label: '操作', slot: 'action', width: '180' }
]

const tableData = [
  // 表格数据...
]
</script>

进阶功能扩展

1、添加分页功能

```<template>
 <div class="pro-table-container">
   <!-- 表格部分 -->
   <el-table ...></el-table>
   
   <!-- 分页部分 -->
   <div class="pagination" v-if="pagination">
     <el-pagination
       v-model:current-page="currentPage"
       v-model:page-size="pageSize"
       :page-sizes="[10, 20, 50, 100]"
       :total="total"
       layout="total, sizes, prev, pager, next, jumper"
       @size-change="handleSizeChange"
       @current-change="handleCurrentChange"
     />
   </div>
 </div>
</template>

<script setup>
// 添加分页相关props和方法
const props = defineProps({
 // ...其他props
 pagination: {
   type: Boolean,
   default: true
 },
 total: {
   type: Number,
   default: 0
 },
 pageSize: {
   type: Number,
   default: 10
 },
 currentPage: {
   type: Number,
   default: 1
 }
})

const emit = defineEmits([
 // ...其他emits
 'update:pageSize',
 'update:currentPage',
 'pagination-change'
])

const handleSizeChange = (val) => {
 emit('update:pageSize', val)
 emit('pagination-change', { pageSize: val, currentPage: props.currentPage })
}

const handleCurrentChange = (val) => {
 emit('update:currentPage', val)
 emit('pagination-change', { pageSize: props.pageSize, currentPage: val })
}
</script>

2、添加表格工具栏

<template>
  <div class="pro-table-container">
    <!-- 工具栏 -->
    <div class="toolbar" v-if="$slots.toolbar || showToolbar">
      <div class="left">
        <slot name="toolbar-left"></slot>
      </div>
      <div class="right">
        <slot name="toolbar-right">
          <el-button type="primary" @click="handleRefresh">
            <el-icon><refresh /></el-icon>
            刷新
          </el-button>
        </slot>
      </div>
    </div>
    
    <!-- 表格部分 -->
    <el-table ...></el-table>
    
    <!-- 分页部分 -->
    <div ...></div>
  </div>
</template>

<script setup>
// 添加工具栏相关props
const props = defineProps({
  // ...其他props
  showToolbar: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits([
  // ...其他emits
  'refresh'
])

const handleRefresh = () => {
  emit('refresh')
}
</script>

3、添加加载状态

<template>
  <el-table
    v-loading="loading"
    element-loading-text="加载中..."
    element-loading-spinner="el-icon-loading"
    element-loading-background="rgba(0, 0, 0, 0.1)"
    ...
  >
    ...
  </el-table>
</template>

<script setup>
const props = defineProps({
  // ...其他props
  loading: {
    type: Boolean,
    default: false
  }
})
</script>

完整封装示例

<template>
  <div class="pro-table-container">
    <!-- 工具栏 -->
    <div class="toolbar" v-if="$slots.toolbar || showToolbar">
      <div class="left">
        <slot name="toolbar-left"></slot>
      </div>
      <div class="right">
        <slot name="toolbar-right">
          <el-button type="primary" @click="handleRefresh">
            <el-icon><refresh /></el-icon>
            刷新
          </el-button>
        </slot>
      </div>
    </div>
    
    <!-- 表格部分 -->
    <el-table
      v-bind="$attrs"
      :data="tableData"
      :border="border"
      :stripe="stripe"
      v-loading="loading"
      element-loading-text="加载中..."
      element-loading-spinner="el-icon-loading"
      element-loading-background="rgba(0, 0, 0, 0.1)"
      @selection-change="handleSelectionChange"
      @sort-change="handleSortChange"
    >
      <template v-for="item in columns" :key="item.prop">
        <!-- 选择列 -->
        <el-table-column v-if="item.type === 'selection'" type="selection" width="55" />
        
        <!-- 序号列 -->
        <el-table-column 
          v-else-if="item.type === 'index'" 
          type="index" 
          :width="item.width || '80'" 
          :label="item.label || '序号'" 
        />
        
        <!-- 普通列 -->
        <el-table-column
          v-else
          :prop="item.prop"
          :label="item.label"
          :width="item.width"
          :min-width="item.minWidth"
          :align="item.align || 'center'"
          :sortable="item.sortable || false"
        >
          <!-- 自定义列内容 -->
          <template #default="scope" v-if="item.slot">
            <slot :name="item.slot" :row="scope.row"></slot>
          </template>
        </el-table-column>
      </template>
    </el-table>
    
    <!-- 分页部分 -->
    <div class="pagination" v-if="pagination">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :page-sizes="pageSizes"
        :total="total"
        :layout="paginationLayout"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>

<script setup>
import { Refresh } from '@element-plus/icons-vue'

const props = defineProps({
  columns: {
    type: Array,
    required: true
  },
  tableData: {
    type: Array,
    required: true
  },
  border: {
    type: Boolean,
    default: true
  },
  stripe: {
    type: Boolean,
    default: true
  },
  loading: {
    type: Boolean,
    default: false
  },
  pagination: {
    type: Boolean,
    default: true
  },
  total: {
    type: Number,
    default: 0
  },
  pageSize: {
    type: Number,
    default: 10
  },
  currentPage: {
    type: Number,
    default: 1
  },
  pageSizes: {
    type: Array,
    default: () => [10, 20, 50, 100]
  },
  paginationLayout: {
    type: String,
    default: 'total, sizes, prev, pager, next, jumper'
  },
  showToolbar: {
    type: Boolean,
    default: true
  }
})

const emit = defineEmits([
  'selection-change',
  'sort-change',
  'update:pageSize',
  'update:currentPage',
  'pagination-change',
  'refresh'
])

const handleSelectionChange = (val) => {
  emit('selection-change', val)
}

const handleSortChange = (val) => {
  emit('sort-change', val)
}

const handleSizeChange = (val) => {
  emit('update:pageSize', val)
  emit('pagination-change', { pageSize: val, currentPage: props.currentPage })
}

const handleCurrentChange = (val) => {
  emit('update:currentPage', val)
  emit('pagination-change', { pageSize: props.pageSize, currentPage: val })
}

const handleRefresh = () => {
  emit('refresh')
}
</script>

<style scoped>
.pro-table-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}

.pagination {
  display: flex;
  justify-content: flex-end;
  margin-top: 16px;
}
</style>

使用技巧与最佳实践

  • 合理设计columns配置:将列配置设计为可扩展的,支持更多自定义选项

  • 插槽灵活运用:通过作用域插槽提供最大限度的自定义能力

  • 性能优化:

    • 大数据量时使用虚拟滚动

    • 合理使用v-if和v-show

    • 避免不必要的响应式数据

  • 错误处理:添加表格数据为空时的空状态提示

  • 国际化支持:考虑将表头标签等文本支持国际化

总结

通过对Element Plus Table组件的二次封装,我们实现了:

  • 统一的表格风格和行为

  • 配置化的表格生成方式

  • 丰富的扩展功能(分页、工具栏、加载状态等)

  • 更高的代码复用率和可维护性

这种封装方式可以显著提高开发效率,特别适合中后台管理系统开发。根据项目实际需求,你还可以进一步扩展更多功能,如列显隐控制、列拖拽排序、导出Excel等。

希望本文对你有所帮助,欢迎在评论区分享你的封装经验和想法!

如果你喜欢这篇文章,欢迎关注我的微信公众号【前端的那点事情】,获取更多精彩内容!