对Element-Plus表格组件二次封装,自定义table组件详解

2,727 阅读2分钟

0.为什么要对Element-Plus组件封装

在进行表格类数据展示时,使用原始的组件库,需要写大量的重复代码,不易于维护。可以考虑将Element-Plus的表格类组件进行一个二次封装,我们仅仅需要往table组件内传入多个参数, 就可以实现表格数据的内容展示。在封装好组件后,对于类似的业务场景,可以直接迁移使用,仅仅需要对参数进行修改即可。

1.如何对表格原始数据进行处理

对Element-Plus组件库进行二次封装,编写一个可扩展的table组件。在很多业务场景中,对于表格内容展示,有时不仅仅要展示文本。可能还需要对原始数据进行二次处理,例如:

  1. 对时间字符串进行格式化
  2. 将原始数据的0或1转换成禁用或启用

除此之外,还有很多应用场景。因此考虑将表格组件进行封装。以下列代码片段为例:截取了应用插槽的代码块。propItem是父组件调用table组件时传入的参数,

具体的数据构成如图所示

我们在中定义了插槽slot,并且传入名字name属性。:name="propItem.slotName"

    1. 当前项不存在slotName,变为默认插槽,那么就显示原本的文本内容{{ scope.row[propItem.prop] }}
    2. 当前项存在slotName,变为具名作用域插槽,作用域插槽可以为父组件传递row属性。这样的话可以在父组件处来针对具体的slotName编写要填入的内容。

以下图为例,对slotName是createAt的插槽,往里填写了内容,主要做了一个时间的格式化工作。

涉及数据二次处理部分的组件代码

<el-table
      :data="listData"
      border
      style="width: 100%"
      @selection-change="handleSelectionChange"
      v-bind="childrenProps"
    >
      <template v-for="propItem in propList" :key="propItem.prop">
        <el-table-column v-bind="propItem" align="center" show-overflow-tooltip>
          <!-- scope.row会存放表格中的所有行的数据,scope.row[propItem.prop]则取出某一行 -->
          <template #default="scope">
            <!-- 作用域插槽,向父组件传递name与row属性 -->
            <!-- 插槽名也是动态的,根据父组件传入的参数slotName决定,
                所以若slotName不存在,则没有name属性,则应该就认作默认插槽
            -->
            <slot :name="propItem.slotName" :row="scope.row">
              {{ scope.row[propItem.prop] }}
            </slot>
          </template>
        </el-table-column>
      </template>
</el-table>

2.添加header与footer

2.1 header部分

table组件不仅仅需要展示基本的数据,还需要提供一些功能性的按钮,因此增加头部区域和底部区域footer,设置了插槽。定义了默认内容,并支持父组件传入新内容。

对于顶部页面header。展示表格名称,以及一个插槽。

<!-- 1.页面顶部header -->
<div class="header">
  <slot name="header">
    <div class="title">{{ title }}</div>
    <div class="handler">
      <!-- header部分的插槽 -->
      <slot name="headerHandler"></slot>
    </div>
  </slot>
</div>

举例说明如何对header部分添加内容。下列是父组件调用table组件时为插槽填入的具体内容,实现了新建数据功能。

<!-- 1.header中的插槽 -->
<template #headerHandler>
  <el-button
    v-if="isCreate"
    type="primary"
    size="medium"
    @click="handleNewClick"
    >新建数据</el-button
  >
</template>

2.2 footer部分

在自定义table组件中,为footer部分的插槽编写了默认内容,提供了一个分页器,需要传入page参数。并绑定了handleSizeChange和handleCurrentChange函数,更改每页数据个数,以及当前页时触发调用。会将对应的数据信息传递给父组件使用。

<!-- 3.页面底部footer -->
<div class="footer" v-if="showFooter">
  <slot name="footer">
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="page.currentPage"
      :page-size="page.pageSize"
      :page-sizes="[10, 20, 30]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="listCount"
    >
    </el-pagination>
  </slot>
</div>

3.组件代码

<template>
  <div class="diy-table">
    <!-- 1.页面顶部header -->
    <div class="header">
      <slot name="header">
        <div class="title">{{ title }}</div>
        <div class="handler">
          <!-- header部分的插槽 -->
          <slot name="headerHandler"></slot>
        </div>
      </slot>
    </div>
    <!-- v-bind="childrenProps" 绑定父级组件传来的contentTableConfig中的配置信息中的一部分,用于实现分级列表 -->
    <!-- 2.表格内容展示 -->
    <el-table
      :data="listData"
      border
      style="width: 100%"
      @selection-change="handleSelectionChange"
      v-bind="childrenProps"
    >
      <!-- 勾选列  和 序号列 根据 showSelectColumn\showIndexColumn 来决定是否显示-->
      <!-- el-table-column来显示表头 -->
      <el-table-column
        v-if="showSelectColumn"
        type="selection"
        align="center"
        width="60"
      ></el-table-column>
      <!-- label表示表头某一列具体名称 -->
      <el-table-column
        v-if="showIndexColumn"
        type="index"
        label="序号"
        align="center"
        width="80"
      ></el-table-column>

      <template v-for="propItem in propList" :key="propItem.prop">
        <el-table-column v-bind="propItem" align="center" show-overflow-tooltip>
          <!-- scope.row会存放表格中的所有行的数据,scope.row[propItem.prop]则取出某一行 -->
          <template #default="scope">
            <!-- 作用域插槽,向父组件传递name与row属性 -->
            <!-- 插槽名也是动态的,根据父组件传入的参数slotName决定,
                所以若slotName不存在,则没有name属性,则应该就认作默认插槽
            -->
            <slot :name="propItem.slotName" :row="scope.row">
              {{ scope.row[propItem.prop] }}
            </slot>
          </template>
        </el-table-column>
      </template>
    </el-table>
    <!-- 3.页面底部footer -->
    <div class="footer" v-if="showFooter">
      <slot name="footer">
        <el-pagination
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="page.currentPage"
          :page-size="page.pageSize"
          :page-sizes="[10, 20, 30]"
          layout="total, sizes, prev, pager, next, jumper"
          :total="listCount"
        >
        </el-pagination>
      </slot>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    title: {
      type: String,
      default: ''
    },
    listData: {
      type: Array,
      required: true
    },
    listCount: {
      type: Number,
      default: 0
    },
    propList: {
      type: Array,
      required: true
    },
    showIndexColumn: {
      type: Boolean,
      default: false
    },
    showSelectColumn: {
      type: Boolean,
      default: false
    },
    page: {
      type: Object,
      default: () => ({ currentPage: 0, pageSize: 10 })
    },
    childrenProps: {
      type: Object,
      default: () => ({})
    },
    showFooter: {
      type: Boolean,
      default: true
    }
  },
  emits: ['selectionChange', 'update:page'],
  setup(props, { emit }) {
    const handleSelectionChange = (value: any) => {
      emit('selectionChange', value)
    }

    const handleCurrentChange = (currentPage: number) => {
      emit('update:page', { ...props.page, currentPage })
    }

    const handleSizeChange = (pageSize: number) => {
      emit('update:page', { ...props.page, pageSize })
    }

    return {
      handleSelectionChange,
      handleCurrentChange,
      handleSizeChange
    }
  }
})
</script>

<style scoped lang="less">
.header {
  display: flex;
  height: 45px;
  padding: 0 5px;
  justify-content: space-between;
  align-items: center;

  .title {
    font-size: 20px;
    font-weight: 700;
  }

  .handler {
    align-items: center;
  }
}

.footer {
  margin-top: 15px;

  .el-pagination {
    text-align: right;
  }
}
</style>