对于ElTable、ElForm 的二次业务封装

116 阅读3分钟

解决el-table、el-form 在使用中重复书写同结构 dom 问题,封装灵感来源于 react、antDesign 彻底实现数据同dom 层隔离 (其实是vue 对于jsx 函数式编程支持)

之前我们使用el-table

// 我们一般使用 table 控件的方式

// 我们的业务场景肯定还会有分页、el-table-column 当然也会很多,还会包含一些接口请求数据处理。想想看这一坨内容有多麻烦我明明什么都没做怎么已经有这么多代码了。当然如果公司是按代码量来计算工作的那我就这样写还可以加绩效(很开心,虽然烦但是有钱)


<template>
  <el-table :data="tableData" style="width: 100%">
    <el-table-column prop="date" label="Date" width="180" />
    <el-table-column prop="name" label="Name" width="180" />
    <el-table-column prop="address" label="Address" />
  </el-table>
  // 业务中的分页处理
  <el-pagination background layout="prev, pager, next" :total="1000" />
</template>

<script lang="ts" setup>
const tableData = [
  {
    date: '2016-05-03',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-02',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-04',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
  {
    date: '2016-05-01',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  },
]

const fetchData = ()=>{
    /// 数据请求
}
</script>

针对上述问题二次封装

组件封装中关键节点

  1. 动态属性绑定 $attrs、v-bind
  2. 渲染函数 & JSX 使用 此种方式完全是我从 react 使用习惯导致个人很喜欢这种方式
<template>
  <div class="table-ui" :style="{ height, padding }">
     <!-- $attrs可以理解为继承,我们应该感谢这个属性对于二次封装极其方便 详见[ $attrs官网介绍  ](https://cn.vuejs.org/api/component-instance.html#attrs)-->
    <el-table v-bind="$attrs" v-loading="hasLoading && loading" :data="fetchPage ? tableData : data"
      :header-cell-style="header_cell_style" :cell-style="cell_style" :height="tableHeight" tooltip-effect="light tit-tip"
      @select="tableSelect" @select-all="tableSelectAll" ref="TableRef">
      <template #empty>
        <div class="no-data" />
      </template>
      <template v-for="(column, index) in columns">
         <!--  v-bind 动态的绑定一个或多个 attribute,也可以是组件的 prop。 详见[ v-bind官网介绍  ](https://cn.vuejs.org/api/built-in-directives.html#v-bind -->
        <el-table-column v-bind="{ ...column.props }" :key="column.dataIndex" :prop="column.dataIndex"
          :label="column.label" :type="column.type" :width="column.width" :fixed="column.fixed"
          :min-width="column.minWidth" v-if="column.render">
          <template #default="scope">
            <!-- 内部组件 使用 jsx 语法解决了不同 el-table-column 插槽问题 -->
            <OptionContent :option="column" :row="scope.row" v-if="column.render" />
          </template>
        </el-table-column>
        <el-table-column v-else v-bind="{ ...column.props }" :key="column.dataIndex + index" :prop="column.dataIndex"
          :label="column.label" :type="column.type" :width="column.width" :index="indexMethod"
          :formatter="column.format || format" :fixed="column.fixed" :min-width="column.minWidth"
          :show-overflow-tooltip="column['show-overflow-tooltip'] || false" />
      </template>
    </el-table>
    <div class="bottom-pagination" v-if="total > 20">
      <el-pagination :current-page="page.page" :page-sizes="[20, 40, 80, 100]" :page-size="page.pageSize"
        layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="sizeChange"
        @current-change="currentChange" />
    </div>
  </div>
</template>
<script>
import { DIALOG_FORM_STYLE } from '@/constants/config';
import { debounce } from '@/utils';
import { format } from '@/utils/formatter';

export default {
  name: 'TableUi',
  props: {
    columns: {
      type: Array,
      default: () => []
    },
    fetchPage: Function,
    data: {
      // 在没有fetchPage时使用,俩个同时存在优先使用fetchPage
      type: Array,
      default: () => []
    },
    height: {
      type: String,
      default: '100%'
    },
    padding: {
      type: String,
      default: '0px'
    },
    params: {
      // 表单搜索条件
      type: Object,
      default: () => { }
    },
    hasLoading: {
      type: Boolean,
      default: true
    },
    cellBg: {
      type: String,
      default: ''
    },
    modelValue: {
      type: Array,
      default: () => []
    }
  },
  emits: ['update:modelValue'],
  components: {
    // 内置组件解决插槽书写的不确定性,函数式和模板式的区别
    OptionContent: {
      props: {
        option: Object,
        row: Object
      },
      render(h) {
        return this.option.render && this.option.render(h, this.row);
      }
    }
  },
  computed: {
    tableHeight() {
      return this.total > 20 ? 'calc(100% - 60px)' : '100%';
    },
    cell_style() {
      let obj = {};
      Object.assign(
        obj,
        DIALOG_FORM_STYLE.CELL_STYLE,
      );
      if (this.cellBg != '') {
        Object.assign(
          obj,
          {
            background: this.cellBg,
          }
        );
      }
      return obj;
    },
    selectCount() {
      return this.modelValue.length
    },
    selectIds: {
      get() {
        return this.modelValue
      },
      set(val) {
        this.$emit('update:modelValue', val)
      }
    }
  },
  data() {
    return {
      header_cell_style: DIALOG_FORM_STYLE.HEADER_CELL_STYLE,
      // cell_style: DIALOG_FORM_STYLE.CELL_STYLE,
      debounceFetchTableData: debounce(this.fetchTableData),
      format,
      tableData: [],
      total: 0,
      loading: false,
      page: {
        pageSize: 20,
        page: 1
      }
    };
  },
  watch: {
    page: {
      handler() {
        this.debounceFetchTableData();
      },
      deep: true
    },
    params: {
      handler() {
        this.page = {
          pageSize: 20,
          page: 1
        };
      },
      deep: true
    }
  },
  mounted() {
    this.fetchPage && this.debounceFetchTableData();
  },
  methods: {
    fetchTableData(params = { ...this.params, ...this.page }) {
      this.loading = this.hasLoading;
      this.fetchPage &&
        this.fetchPage(params)
          .then(res => {
            this.total = res.totalCount;
            this.tableData = res.list;
            this.loading = false;
            this.toggleRowSelection()
          }).catch(e => {
            this.loading = false;
          });
    },
    currentChange(val) {
      if (this.page.page !== val) {
        this.tableData = [];
        this.page.page = val;
      }

    },
    sizeChange(val) {
      this.page.pageSize = val;
    },
    indexMethod(idx) {
      const cacheIdx = (this.page.page - 1) * this.page.pageSize;
      const curIdx = idx + 1;
      return cacheIdx + curIdx;
    },
    tableSelect(selection, row) {
      if (this.selectIds.includes(row.equipCode)) {
        this.selectIds = this.selectIds.filter(item => item !== row.equipCode)
      } else {
        this.selectIds.push(row.equipCode)
      }
    },
    tableSelectAll(selection) {
      const selectIds = selection.map(item => item.equipCode);
      if (selection.length) {
        selectIds.forEach(equipCode => {
          !this.selectIds.includes(equipCode) && this.selectIds.push(equipCode)
        })
      } else {
        this.selectIds = this.selectIds.filter(id => !this.tableData.map(data => data.equipCode).includes(id))
      }
    },
    toggleRowSelection() {
      this.$refs.TableRef.clearSelection()
      this.$nextTick(() => {
        if (this.selectIds && this.selectIds.length) {
          const rows = this.tableData.filter(data => this.selectIds.includes(data.equipCode))
          rows.forEach(row => {
            this.$refs.TableRef.toggleRowSelection(row, true)
          })
        }
      })
    },
    /**
     * @description: 手动刷新请求
     * @param {Bool} isResetFetch 是否重置请求
     * @param {Bool} isComputedPage 是否自动计算页数(删除、编辑、新增时候)
     * @return {void}
     */
    refresh({ isResetFetch = false, isComputedPage = true }) {
      if (isResetFetch) {
        this.page = {
          pageSize: 20,
          page: 1
        };
      } else {
        if (isComputedPage) this.page.page = this.tableData.length === 1 && this.page.page !== 1 ? this.page.page - 1 : this.page.page;
      }
      isResetFetch ? this.debounceFetchTableData(this.page) : this.debounceFetchTableData();
    }
  }
};
</script>
接下来就是项目中的使用
// 具体需要查看你组件所在的位置这里只是测试
import Table from 'Table';

/* 页面使用仅需3个参数 columns、fetchPage、params */
// columns table中渲染的列
// fetchPage 请求的接口
// params 接口可搜索参数 可配合el-form使用

<Table ref="TableRef" :columns="columns" :fetchPage="userApi.getUser" :params="params" />

// 测试数据
const columns = [
  { dataIndex: 'index', label: '序号', type: 'index', width: '80', fixed: 'left' },
  { dataIndex: 'account', label: '账号', fixed: 'left', minWidth: '100' },
  { dataIndex: 'nickname', label: '姓名', fixed: 'left', minWidth: '100' },
  { dataIndex: 'orgName', label: '组织', minWidth: '180', width: '180' },
  { dataIndex: 'position', label: '职务', minWidth: '100', format: formatPosition },
  { dataIndex: 'roleNames', label: '角色', minWidth: '180', format: formatArrayStr, 'show-overflow-tooltip': true },
  { dataIndex: 'phoneNumber', label: '联系方式', minWidth: '150', width: '150' },
  { dataIndex: 'createTime', label: '创建时间', minWidth: '180', width: '180' },
  {
    dataIndex: 'action',
    label: '操作',
    fixed: 'right',
    minWidth: '150',
    render: (h, params) => {
      return (
        <div class="action">
          <el-icon size="20" title="详情" class={params.account === 'admin' ? 'disabled action-icon' : 'action-icon'} onClick={() => details(params)}>
            <Tickets />
          </el-icon>
          <el-icon size="20" title="编辑" class={params.account === 'admin' ? 'disabled action-icon' : 'action-icon'} onClick={() => editRole(params)}>
            <Edit />
          </el-icon>
          <el-icon size="20" title="重置密码" class={params.account === 'admin' ? 'disabled action-icon' : 'action-icon'} onClick={() => editPassword(params)}>
            <Key />
          </el-icon>
          <el-icon size="20" title="删除" class={params.account === 'admin' ? 'disabled action-icon' : 'action-icon'} onClick={() => deleteUser(params)}>
            <Delete />
          </el-icon>
        </div>
      )
    }
  }
]

const params = ref({})

已同步发布 npmgithub,希望可以给大家带来帮助。有什么不足请指正。关于el-form的二次封装在仓库中也是有的。文档后续会补充。哈哈哈哈哈,标题不是骗人的哈