基于 Element Plus 的 Table 组件二次封装实践

引言

在 Vue 3 + Element Plus 的项目开发中,表格(Table)是最常用的组件之一。为了提升开发效率,减少重复代码,我们常常需要对 Element Plus 的 Table 组件进行二次封装。本文将介绍一个实用的 Table 组件封装方案,该方案集成了分页、选择、自定义列等功能,可以显著提升开发效率。

组件特点

  1. 灵活的自适应高度
  • 自动计算表格高度,适应不同页面布局
  • 支持自定义高度设置
  • 响应式调整,适应窗口大小变化
  1. 丰富的列类型支持
  • 支持图片列(可预览)
  • 支持自定义 HTML 渲染
  • 支持自定义插槽
  • 支持可点击的按钮列
  • 支持自定义样式
  1. 完善的选择功能
  • 支持多选和单选模式
  • 支持自定义选择条件
  • 支持点击行选择
  • 支持保留选择状态
  1. 内置分页功能
  • 支持页码切换
  • 支持每页条数调整
  • 支持总数显示
  • 支持快速跳转
js 复制代码
<template>
  <div class="table-header">
    <div>
      <slot name="left" />
    </div>
    <div>
      <slot name="right" />
    </div>
  </div>
  <el-table
    :data="data"
    stripe
    @selection-change="handleSelectionChange"
    @row-click="handleCurrentChange"
    :height="height || `calc(100vh - ${subtractHeight}px`"
    v-loading="loading"
    ref="tableRef"
    class="yh-table"
    :class="single ? 'isSingle' : ''"
    :row-key="rowKey"
  >
    <el-table-column
      type="selection"
      width="55"
      v-if="selection"
      :selectable="selectable"
      :reserve-selection="reserveSelection"
      fixed="left"
      align="center"
    />
    <el-table-column
      type="index"
      width="80"
      align="center"
      label="序号"
      :index="indexMethod"
      v-if="index"
    />
    <el-table-column
      v-for="col in columns"
      :prop="col.prop"
      :key="col.prop"
      :label="col.label"
      :min-width="col.minWidth"
      :width="col.width"
      show-overflow-tooltip
      :formatter="col.formatter"
      align="center"
      :fixed="col.fixed"
    >
      <template #default="{ row }">
        <div v-if="col.type === 'img'">
          <el-image
            v-for="img in row[col.prop]?.split(',')"
            :src="baseUrl + '/common/previewImage?avatar=' + img"
            style="width: 50px"
            preview-teleported
            :preview-src-list="[baseUrl + '/common/previewImage?avatar=' + img]"
          >
            <template #error>
              <span></span>
            </template>
          </el-image>
        </div>
        <div v-else-if="col.html" v-html="col.html(row)"></div>
        <slot v-else-if="col.slot" :name="col.slot" :row="row" />
        <div v-else-if="col.click">
          <el-button link type="primary" size="small" @click="col.click(row)">
            {{ row[col.prop] }}
          </el-button>
        </div>
        <span v-else :style="col.style">
          {{ col.formatter ? col.formatter(row) : row[col.prop] }}
        </span>
      </template>
    </el-table-column>
    <slot></slot>
  </el-table>
  <div class="yh-pagination" v-if="showPagination">
    <el-pagination
      @size-change="sizeChange"
      @current-change="currentChange"
      :currentPage="modelValue.pageNum"
      :page-size="modelValue.pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      size="small"
    />
  </div>
</template>

<script setup>
import { reactive } from 'vue';
const baseUrl = import.meta.env.VITE_PROXY;
const props = defineProps({
  data: {
    type: Array,
    default: () => []
  },
  columns: {
    type: Array,
    default: () => []
  },
  //是否多选
  selection: {
    type: Boolean,
    default: false
  },
  total: {
    type: Number,
    default: 0
  },
  loading: {
    type: Boolean,
    default: false
  },
  //自定义表格高度
  height: {
    type: String,
    default: undefined
  },
  //额外需要减去的高度
  offsetHeight: {
    type: Number,
    default: 0
  },
  //分页
  modelValue: {
    type: Object,
    default: () => {
      return {
        pageNum: 1,
        pageSize: 20
      };
    }
  },
  //是否可选判断条件
  selectable: {
    type: Function,
    default: () => true
  },
  //是否展开搜索栏
  showSearch: {
    type: Boolean,
    default: false
  },
  //单选
  single: {
    type: Boolean,
    default: false
  },
  reserveSelection: {
    type: Boolean,
    default: false
  },
  rowKey: {
    type: String,
    default: ''
  },
  //是否展示序号
  index: {
    type: Boolean,
    default: true
  },
  //是否显示分页
  showPagination: {
    type: Boolean,
    default: true
  },
  //是否需要点击单元格选中数据
  needClickCell: {
    type: Boolean,
    default: true
  }
});

const selectedList = ref([]);
const handleCurrentChange = (row) => {
  if (!props.selection || !props.needClickCell || !props.rowKey) return;
  const index = selectedList.value.findIndex(
    (item) => item[props.rowKey] === row[props.rowKey]
  );
  if (index === -1) {
    selectedList.value.push(row);
    tableRef.value.toggleRowSelection(row, true);
  } else {
    selectedList.value.splice(index, 1);
    tableRef.value.toggleRowSelection(row, false);
  }
};
const emits = defineEmits([
  'update:modelValue',
  'pageChange',
  'selection-change'
]);
const tableRef = ref(null);
//序号
const indexMethod = (index) => {
  return props.modelValue.pageSize * (props.modelValue.pageNum - 1) + index + 1;
};

//多选
const handleSelectionChange = (val) => {
  if (props.single && val.length > 1) {
    tableRef.value.clearSelection();
    tableRef.value.toggleRowSelection(val.pop(), true);
    return;
  }
  selectedList.value = val;
  emits('selection-change', val);
};

//分页
const sizeChange = (val) => {
  const page = { ...props.modelValue };
  page.pageSize = val;
  emits('update:modelValue', page);
  emits('pageChange');
};
const currentChange = (val) => {
  const page = { ...props.modelValue };
  page.pageNum = val;
  emits('update:modelValue', page);
  emits('pageChange');
};

onMounted(() => {
  getSearchFormHeight();
  window.addEventListener('resize', getSearchFormHeight);
});
//table需要减去的高度
const subtractHeight = ref(50);
const getSearchFormHeight = () => {
  nextTick(() => {
    // 页面头部高度
    const pageHeaderHeight =
      document.querySelector('.main-hearder-top')?.offsetHeight || 0;
    // 分页高度
    const pageHeight =
      document.querySelector('.yh-pagination')?.offsetHeight || 0;
    // 搜索表单高度
    const searchformHeight =
      document.querySelector('.search-form')?.offsetHeight || 0;
    // 工具栏高度
    const toolsHeight =
      document.querySelector('.table-header')?.offsetHeight || 0;
    // 减去的高度 35=>页面内边距padding值
    subtractHeight.value =
      searchformHeight + pageHeight + pageHeaderHeight + toolsHeight + 43;
  });
};

watch(
  () => props.showSearch,
  () => {
    getSearchFormHeight();
  }
);
onUnmounted(() => {
  window.removeEventListener('resize', getSearchFormHeight);
});
</script>

<style lang="scss" scoped>
.table-header {
  display: flex;
  justify-content: space-between;
  padding: 5px 0;
}
.yh-pagination {
  display: flex;
  justify-content: flex-end;
  margin-top: 10px;
}
.isSingle :deep(.el-table__header-wrapper .el-checkbox) {
  display: none;
}
</style>

示例

js 复制代码
<template>
  <div class="table-container">
    <!-- 搜索表单 -->
    <div class="search-form">
      <el-form :inline="true">
        <el-form-item label="姓名">
          <el-input v-model="searchForm.name" placeholder="请输入姓名" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
        </el-form-item>
      </el-form>
    </div>

    <!-- 表格工具栏 -->
    <div class="table-header">
      <div>
        <el-button type="primary" @click="handleAdd">新增</el-button>
        <el-button type="danger" @click="handleBatchDelete">批量删除</el-button>
      </div>
      <div>
        <el-button @click="handleExport">导出</el-button>
      </div>
    </div>

    <!-- 表格 -->
    <yh-table
      :data="tableData"
      :columns="columns"
      :total="total"
      v-model:modelValue="page"
      selection
      :row-key="'id'"
      :loading="loading"
      @selection-change="handleSelectionChange"
      @pageChange="handlePageChange"
    >
      <template #operation="{ row }">
        <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
        <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
      </template>
    </yh-table>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

// 搜索表单
const searchForm = ref({
  name: ''
});

// 表格数据
const tableData = ref([]);
const loading = ref(false);
const total = ref(0);

// 分页数据
const page = ref({
  pageNum: 1,
  pageSize: 20
});

// 选中的数据
const selectedRows = ref([]);

// 列配置
const columns = [
  {
    prop: 'name',
    label: '姓名',
    width: '120'
  },
  {
    prop: 'avatar',
    label: '头像',
    type: 'img'
  },
  {
    prop: 'age',
    label: '年龄',
    width: '100'
  },
  {
    prop: 'status',
    label: '状态',
    formatter: (row) => row.status === 1 ? '正常' : '禁用'
  },
  {
    prop: 'score',
    label: '分数',
    style: (row) => ({
      color: row.score >= 60 ? 'green' : 'red',
      fontWeight: 'bold'
    })
  },
  {
    prop: 'detail',
    label: '详情',
    click: (row) => {
      console.log('查看详情:', row);
    }
  },
  {
    prop: 'operation',
    label: '操作',
    slot: 'operation'
  }
];

// 方法
const fetchData = async () => {
  loading.value = true;
  try {
    // 模拟API调用
    const response = await fetchDataFromApi({
      ...searchForm.value,
      ...page.value
    });
    tableData.value = response.data;
    total.value = response.total;
  } catch (error) {
    console.error('获取数据失败:', error);
  } finally {
    loading.value = false;
  }
};

const handleSearch = () => {
  page.value.pageNum = 1;
  fetchData();
};

const resetSearch = () => {
  searchForm.value = {
    name: ''
  };
  handleSearch();
};

const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};

const handleAdd = () => {
  console.log('新增');
};

const handleEdit = (row) => {
  console.log('编辑:', row);
};

const handleDelete = (row) => {
  console.log('删除:', row);
};

const handleBatchDelete = () => {
  if (selectedRows.value.length === 0) {
    ElMessage.warning('请选择要删除的数据');
    return;
  }
  console.log('批量删除:', selectedRows.value);
};

const handleExport = () => {
  console.log('导出');
};

// 生命周期
onMounted(() => {
  fetchData();
});
</script>

<style lang="scss" scoped>
.table-container {
  padding: 20px;
  
  .search-form {
    margin-bottom: 20px;
  }
  
  .table-header {
    margin-bottom: 10px;
  }
}
</style>
相关推荐
GISer_Jing37 分钟前
前端工程化和性能优化问题详解
前端·性能优化
学渣y1 小时前
React文档-State数据扁平化
前端·javascript·react.js
njsgcs1 小时前
画立方体软件开发笔记 js three 投影 参数建模 旋转相机 @tarikjabiri/dxf导出dxf
前端·javascript·笔记
一口一个橘子1 小时前
[ctfshow web入门] web71
前端·web安全·网络安全
逊嘘1 小时前
【Web前端开发】HTML基础
前端·html
未脱发程序员2 小时前
【前端】每日一道面试题3:如何实现一个基于CSS Grid的12列自适应布局?
前端·css
三天不学习2 小时前
Visual Studio Code 前端项目开发规范合集【推荐插件】
前端·ide·vscode
爱分享的程序猿-Clark3 小时前
【前端分享】CSS实现3种翻页效果类型,附源码!
前端·css
Code哈哈笑3 小时前
【图书管理系统】深度讲解:图书列表展示的后端实现、高内聚低耦合的应用、前端代码讲解
java·前端·数据库·spring boot·后端
无名之逆3 小时前
Hyperlane: Unleash the Power of Rust for High-Performance Web Services
java·开发语言·前端·后端·http·rust·web