vue3封装element-table,可以食用吧

140 阅读3分钟

前言

入职新公司接近一个月了,公司的项目后台管理系统居多,看了公司的代码,基本没有对顶部的搜索栏部分和表格的二次封装,只是直接使用了Element提供的组件,直接使用当然的就代码看起来很直观,不过就是可能会导致代码会很长,最近接手了公司的项目,其中有一个页面有1500多行,说实话我有点受不了,所以就试着封装一下搜索栏和表格,这次先讲table的封装,搜索栏的后面看时间。

二次封装是否有必要,其实看自己,封装的话前期的时间成本比较高,掘金上也有吐槽二次封装的弊端# 拒绝!封装el-table,请别再用JSON数组来配置列了,这个没有好坏,主要是经历比较重要。

正文

封装table组件的需求点,我就称二次封装的table是CommonTable了,如下图。

也许还要更多的需求点,就看你咋扩展了。

  1. 自定义表格表头
  2. 自定义element提供列,比如单选,序号
  3. 正常的列显示
  4. 自定义列表内容,比如操作这一栏等

image.png

熟悉element-table提供的表格的话,你基本就知道这里面最重要的就是tableData和prop值了,那么基本就是围绕着这俩个属性封装了。

tableData值好说,就在页面接口请求,返回值,然后传给CommonTable就是。

prop的话,看下图,有好多个columnn,prop不同,所以确认还要传一个数组给CommonTable

image.png

需求点3

基本代码如下,tableData和columns就是我们要传给CommonTable的属性,可以看到我在el-table标签和el-table-column标签上写上了 v-bind,至于$attrs是啥意思,请阅读vue官方,主要是为了将element提供的api也应用到CommonTable中,比如element给el-table标签提供了许多方法等。

<el-table :data="tableData" v-bind="$attrs">
    <template v-for="(item, index) in columns" :key="item.prop + index">
        <el-table-column v-bind="{ ...item }">
            ...
        </el-table-column>
    </template>
</el-table>

<script setup>
const prop = defineProps({
  tableData: {
    type: Array,
    default() {
      return [];
    },
  },
  column: {
    type: Array,
    default: () => [],
  },
});
const tableData = ref([]);
const columns = ref([]);
const typeLists = ref(["slotHeader", "slot", "dataMap", "slotBtn"]);

//为啥要watch在重新赋值一遍呢,其实只是为了保证数据最新罢了,不做可能也可以
watch(
  () => prop.tableData,
  (newVal, oldVal) => {
    tableData.value = newVal;
  },
  { deep: true, immediate: true }
);
watch(
  () => prop.columns,
  (newVal, oldVal) => {
    columns.value = newVal;
  },
  { deep: true, immediate: true }
);
</script>

columns是一个数组,遍历循环这个数据,v-bind="{ ...item }会是我们传入的属性,从上图可以看到el-table-column需要一个label和prop,所以columns的数据是这样的

到这基本就实现了上面的需求点3了

const column = ref([
  { label: '姓名', prop: "realName"},
  { label: '手机号码', prop: "mobile"},
  { label: '用户类型'prop: "userRiskType" }
  {...}
]);

需求点2

这里主要是增加一个type属性,比较简单

el-table-column这里我还加了一些比较常用的属性,单独拎出来

<el-table :data="tableData" v-bind="$attrs">
    <template v-for="(item, index) in columns" :key="item.prop + index">
        <el-table-column v-bind="{ ...item }"
        :align="item.align ? item.align : 'center'"
        :header-align="item.headerAlign ? item.headerAlign : 'center'"
        :show-overflow-tooltip="item.showTooltip ? item.showTooltip : false"
        :fixed="item.fixed ? item.fixed : false">
            ...
        </el-table-column>
        
        <!-- 格式化类型,例如第一列单选框 -->
        <el-table-column v-if="item.type" :key="item.prop + index" v-bind="{ ...item }" />
    </template>
</el-table>
const column = ref([
  { type: "selection"prop: "select"},//新增一列
  { label: '姓名', prop: "realName"},
  { label: '手机号码', prop: "mobile"},
  { label: '用户类型'prop: "userRiskType" }
  {...}
]);

需求点1

element原生的自定义表头写法是这样的,所以我们使用插槽控制一下自定义的内容就行,结合到我们的组件代码中

<el-table-column align="right">
      <template #header>
        <el-input v-model="search" size="small" placeholder="Type to search" />
      </template>
</el-table-column>

如下,定义一个columnType,只有当columns数组项中有slotHeader才可以实现,通过具名插槽,同时也可以传入row,如果表头涉及列表数据的话。如果不传,就默认返回label表头。

<el-table :data="tableData" v-bind="$attrs">
    <template v-for="(item, index) in columns" :key="item.prop + index">
       <el-table-column v-bind="{ ...item }"
        :align="item.align ? item.align : 'center'"
        :header-align="item.headerAlign ? item.headerAlign : 'center'"
        :show-overflow-tooltip="item.showTooltip ? item.showTooltip : false"
        :fixed="item.fixed ? item.fixed : false">
            <!-- 表头 -->
        <template #header="{ row }">
          <slot v-if="item.columnType === 'slotHeader'" :name="item.prop + 'Header'" :row="row"></slot>
          <template v-else>{{ item.label }}</template>
        </template>
       </el-table-column>
    </template>
</el-table>

具体使用如下,需求点1基本就完成了

<CommonTable :table-data="tableData" :columns="columns" >
      //注意插槽prop属性+'Header'
      <template #WAHeader> 
        <el-button> WA </el-button>
      </template>
</CommonTable>

const column = ref([
  {prop: "WA",label: "WA",minWidth: "105",columnType: "slotHeader"}
)]

需求点4

自定义列表格的内容,这里也是插槽,先看原生如下,其实和自定义表头差不多,不过这里可以根据自己的业务进行直接的扩展。

<el-table-column label="结果描述" align="center" prop="resultDesc">
        <template slot-scope="scope">
           //自定义内容 
          <dict-tag :options="dict.type.pay_order_result_desc" :value="scope.row.resultDesc"/>
        </template>
</el-table-column>

根据自己的业务代码进行扩展,比如很多时候进行字典映射的匹配,公共的按钮等

<el-table :data="tableData" v-bind="$attrs">
    <template v-for="(item, index) in columns" :key="item.prop + index">
       <el-table-column v-bind="{ ...item }">
        <!-- 自定义内容 -->
        <template #default="{ row }">
          <!-- 默认返回 -->
          <template v-if="!item.columnType">
            {{ row[item.prop] }}
          </template>
          <!-- 插槽 -->
          <slot v-else-if="item.columnType === 'slot' || item.columnType === 'slotHeader'" 
          :name="item.prop" :row="row" />
          <!-- 业务扩展,数据字典映射 -->
          <template v-else-if="item.columnType === 'dataMap'">
          {{ item.dataMap[row[item.prop]] || "" }}</template> 
        </template>
       <el-table-column>
    </template>
</el-table>

具体使用如下,自此4个需求点基本完成。

<CommonTable:table-data="tableData" :column="column" @clickHandle="clickHandle">
//通过上面的代码可以知道prop值就是我们的具名插槽
      <template #operate="{ row }">
        <el-button>编辑</el-button>
        <el-button>删除</el-button>
      </template>
</CommonTable>

完整代码及使用

CommonTable

<!-- 
  isUnShow:控制表格列的隐藏和展示,默认展示 
  showTooltip:悬浮弹窗 
  fixed:表格列浮动
  columnType:slotHeader表头自定义、slot表格某列自定义、dataMap数据映射
-->
<template>
  <el-table :data="tableDataLocal" v-bind="$attrs">
    <template v-for="(item, index) in columnLocal" :key="item.prop + index">
      <el-table-column
        v-if="!item.isUnShow && !item.type && (!item.columnType || typeLists.indexOf(item.columnType) > -1)"
        v-bind="{ ...item }"
        :align="item.align ? item.align : 'center'"
        :header-align="item.headerAlign ? item.headerAlign : 'center'"
        :show-overflow-tooltip="item.showTooltip ? item.showTooltip : false"
        :fixed="item.fixed ? item.fixed : false">
        <!-- 表头 -->
        <template #header="{ row }">
          <slot v-if="item.columnType === 'slotHeader'" :name="item.prop + 'Header'" :row="row"></slot>
          <template v-else>{{ item.label }}</template>
        </template>
        <!-- 插槽 -->
        <template #default="{ row, $index }">
          <template v-if="!item.columnType">
            {{ row[item.prop] }}
          </template>
          <slot v-else-if="item.columnType === 'slot' || item.columnType === 'slotHeader'" :name="item.prop" :row="row" :$index="$index" />
          <!-- 数据字典映射 -->
          <template v-else-if="item.columnType === 'dataMap'">{{ item.dataMap[row[item.prop]] || "" }}</template>

        </template>
      </el-table-column>
      <!-- 格式化类型,例如第一列单选框 -->
      <el-table-column v-if="item.type" :key="item.prop + index" v-bind="{ ...item }" :align="item.align ? item.align : 'left'" />
    </template>
  </el-table>
</template>

<script setup>
const prop = defineProps({
  tableData: {
    type: Array,
    default() {
      return [];
    },
  },
  column: {
    type: Array,
    default: () => [],
  },
});
const tableDataLocal = ref([]);
const columnLocal = ref([]);
const typeLists = ref(["slotHeader", "slot", "dataMap"]);

watch(
  () => prop.tableData,
  (newVal, oldVal) => {
    tableDataLocal.value = newVal;
  },
  { deep: true, immediate: true }
);
watch(
  () => prop.column,
  (newVal, oldVal) => {
    columnLocal.value = newVal;
  },
  { deep: true, immediate: true }
);
</script>

<style lang="scss" scoped></style>

index.vue

<template>
  <div class="app-container">
    <CommonTable v-loading="loading" :table-data="tableData" :column="column" @selection-change="selectionChange">
      <template #WAHeader>
        <el-button> WA </el-button>
      </template>
      <template #WA="{ row }">
        <el-button> WA </el-button>
      </template>
      <template #operate="{ row }">
        <el-button>编辑</el-button>
        <el-button>删除</el-button>
      </template>
    </CommonTable>
  </div>
</template>

<script setup>
import CommonTable from "@/components/CommonTable";

const loading = ref(false);
const tableData = ref([]);

const column = ref([
  { prop: "select", type: "selection", minWidth: "100", fixed: "left" },
  {
    prop: "WA",
    label: "WA",
    minWidth: "105",
    columnType: "slotHeader",
  },
  {
    prop: "mobile",
    label: "电话",
    minWidth: "105",
  },
  {
    prop: "applyDate",
    label: "反馈时间",
    minWidth: "105",
  },
  {
    prop: "text",
    label: "问题描述",
    minWidth: "105",
  },
  {
    prop: "operate",
    label: "操作",
    minWidth: "105",
    columnType: "slot",
  },
]);

// 获取列表
const getList = () => {
  loading.value = true;
  getList({ ...param.value })
    .then(({ code, rows, total }) => {
      if (code === 200) {
        tableData.value = rows;
        queryParams.total = total;
      }
    })
    .finally(() => (loading.value = false));
};

getList();
</script>

最后

我还封装了CommonSearch,有空在写写

上面的代码应该是可以直接用的

是否二次封装,看你自己哇,观点不同,如果能提高你的工作的效率,我觉得那就是个好东西,至于你封装的组件其他人看不看得懂,看不懂不是才好hhh。还有这个CommonTable还看不懂,那就。。。。