乐趣区

组件化页面封装eltable

项目做的越来越多,重复的东西不断的封装成了组件,慢慢的,页面就组件化了,只需要定义组件配置数据,使用就好了,这是一件非常舒服的事情,这篇文章主要和大家讲讲如何对 element-ui 中的 el-table 进行二次封装。

分析需求

公有组件,可以被任何页面调用,首先要保证组件不和外部业务发生耦合,其次就是要设计合理的字段,使用时通过不同的配置即可使用。
那先大致来分析以下可能有的需求:

  • 动态表头
  • 嵌套表头
  • 表格显示内容类型自定义(文字,图片,超链接等)
  • 动态接口加载数据
  • 表格和分页联动
  • 分页和查询数据联动
  • 表格事件的处理
  • className, width, height…
  • 更多需求 …

目前封装的组件并不算完美,不可能满足所以需求,这里的话主要还是和大家分享思路

动态表头和嵌套表头的实现

实现动态表头,这个应该是许多使用 table 的朋友们的痛点,明明是一样的东西,却要写多个表格,实在不能忍,让我们今天来一举歼灭它。

分析表头结构

el-table 表头有两个必须的属性,prop 值和 label 名字,其他非必须的有 fixed,align,width 或者 min-width 等,那由此可设计出一个这样的数据结构:

{
    prop: 'name',
    label: '名称',
    fixed: true/false,
    align: 'center',
    minWidth: 160
}

进阶 -> 嵌套表格

上面我们得出了普通表头列的设计,那我们继续分析,看看嵌套表格配置多了哪些字段。
根据 element-ui 官网文档,可以看到前面字段基本一样,嵌套表格多了 children 字段,用来循环子级表头,那由此我们可以设计出这样的数据结构:

{
    prop: 'name',
    label: '名称',
    fixed: true/false,
    align: 'center',
    minWidth: 160,
    children: [
        {
            prop: 'oldName',
            label: '旧名称',
            fixed: true/false,
            align: 'center',
            minWidth: 160,
        },{
            prop: 'newName',
            label: '新名称',
            fixed: true/false,
            align: 'center',
            minWidth: 160,
        }
    ]
}

表头设计总结

表头设计思路大概是这样,并不复杂,根据业务需求,大家都可以设计适合自己使用的字段。

完整的表头设计字段应该大概会是这个样子这个是个人字段配置的例子,其中将 prop 字段改成了 value, 下面代码统一会使用 value 代替 prop

fieldList: [{ label: '账号', value: 'account'},
          {label: '用户名', value: 'name'},
          {label: '所属角色', value: 'role_name', minWidth: 120},
          {label: '性别', value: 'sex', width: 80, list: 'sexList'},
          {label: '账号类型', value: 'type', width: 100, list: 'accountTypeList'},
          {label: '状态', value: 'status', width: 90, type: 'slot', list: 'statusList'},
          {label: '创建人', value: 'create_user_name'},
          {label: '创建时间', value: 'create_time', minWidth: 180},
          {label: '更新人', value: 'update_user_name'},
          {label: '更新时间', value: 'update_time', minWidth: 180}
        ]

表格显示内容类型自定义

表头设计只是将一些基本的需求实现了,但是实际业务往往更为复杂,比如当前列要显示的是图片,tag,超链接,或者列的数据是一个 id 要显示对应的 label。

字段列表扩展

之前定义的字段列表都是简单的文字显示,当有了不同的类型显示需求,则意味着需要一个类型字段,type,根据业务需求,可以设计满足 image,tag,href 等。
字段设计为 type 为 image 时,同时可以考虑设计 width 和 height 字段。
字段设计为 href 时,可以同时设计颜色,跳转方式字段。
比如:

{label: '设备信息', prop: 'deviceInfo', type: 'href', herf: 'https://www.baidu.com', target: '_blank'},
{label: '设备图标', prop: 'deviceImage', type: 'image', src: 'https://www.baidu.com', height: '60px', width: 'auto'}

当列的数据是一个 id 的时候需要显示对应的 label,情况又稍微复杂了一点,多种实现方法:

  • 获取到表格数据后对数据做处理,这个比较简单,但需要在组件外部操作(不推荐)
  • 将对应的列表传入组件中,在组件内部进行转换(推荐)
  • 设置为 slot(好用,但建议使用在复杂的自定义场景,这个在下面会细讲)

讲讲第二种方式,将对应的列表传入组件中,在组件内部进行转换,需要设置当前字段的类型为 id 转换为 label 的类型,我在字段上定义的是 type: select,然后要定义相关的 list,字段设计大概长这样:

{label: '菜单组件', value: 'component', type: 'select', list: 'componentList1'}

我的实现方式是定义了一个 listType 对象,然后把页面上用到的 list 都挂在了这个对象上面,将 listType 传入到 table 组件中,通过 listType[item.list] 可以获取到字段对应列表然后获取对应的 label 显示。

slot

非常非常非常重要的 slot,特别提醒大家,如果想写复杂的组件,考虑到自定义类型,请一定去了解 slot 不了解的请戳
vue2.6+ 已经废弃 slot-scope 官网 api 描述

插槽

  • 父级可以向组件内部传入 dom,组件内部通过插槽接收
  • 渲染方式 1: dom 使用父级数据渲染,传入组件
  • 渲染方式 2: dom 使用组件内部插槽穿出的数据渲染,再传入组件

匿名插槽

父级在使用组件的时候,在组件标签内编写内容,将会组件内部 <solt><slot/> 接收到

具名插槽

父级设置传入的插槽的名字,组件内部匹配到名字相同的插槽进行渲染。
组件内部具名插槽传输数据到父级(dom 接收方,数据传出方):

          <!-- solt 自定义列 -->
          <template v-if="item.type ==='slot'">
            <slot
              :name="'col-' + item.value":row="scope.row"
            />
          </template>

父级获取插槽数据渲染 dom(dom 传出方,数据接收方):

<!-- 自定义插槽显示状态 -->
      <template v-slot:col-status="scope">
        <i
          :class="scope.row.status === 1 ?'el-icon-check':'el-icon-close'":style="{color: scope.row.status === 1 ? '#67c23a' : '#f56c6c', fontSize: '20px'}"
        />
      </template>

总结

slot 是自定义组件的神器。
回到 table 组件,我们需要自定义显示内容,设计的字段应该如下:

{label: '菜单图标', value: 'icon', type: 'slot'}

动态接口加载数据

上面说的都是显示字段设计的东西,现在开始分析表格的数据,从哪里来,到哪里去。

如果要偷懒,那么一定是要把懒偷到底的,有一丁点多余的工作要做,都是偷懒不成功的。

组件内部加载数据

需要什么:

  • 接口
  • 数据响应成功后在 response 的哪个字段上面
  • 怎么刷新接口
  • 是否分页,分页初始化

接口

定义一个 api 字段,将需要请求的接口传入到组件中,如果有相关参数,需要同时将参数传入到组件中

数据所在字段

定义一个 resFieldList,比如数据在 res.content.data 上,则传入数据:

resFieldList: ['content',‘data’] // 数据所在字段 

组件内部则需要在接口请求成功之后做这样一步操作:

          let resData = res
          const resFieldList = tableInfo.resFieldList
          // 得到定义的响应成功的数据字段
          for (let i = 0; i < resFieldList.length; i++) {resData = resData[resFieldList[i]]
          }

数据获取成功之后,建议使用父子组件双向通信,.sync 或者自定义 model 都可以实现,将数据派发到父组件,然后由父组件传入子组件渲染组件。
直接由组件内部获取数据并且渲染可能会需要扩展等问题限制组件的使用范围。

刷新接口

定义一个 refresh 字段,刷新页面只需要设置为:

// 刷新表格
tableInfo.refresh = Math.random()

而组件内部 watch 字段 change,重新调获取数据的接口,即可实现刷新功能

分页相关设置

  • 是否分页,设置字段比如 pager: true/false
  • 是否初始化分页,设置字段比如 initCurpage = Math.random() // 刷新则重置

组件事件处理

分析有哪几种类型的事件:

  • 表头点击事件
  • 列点击事件
  • 表格操作栏点击事件
  • 多选
  • ….

事件中间件的设计

不同的业务可能涉及到各种类型的事件,如果封装成为了组件,怎么处理???
换一个思路,我们把事件看作是一个类型操作,比如点击是 click,删除是 delete,那我们只需要一个事件转发器,比如:

// 数据渲染事件的派发
this.$emit('handleEvent', 'list', arr)
// 表格选择事件的派发
this.$emit('handleEvent', 'tableCheck', rows)
// 点击事件的派发
this.$emit('handleClick', event, data)

我们定义事件中间件,组件内部发生事件时将事件的类型还有相关的数据派发,父级接收并且处理。

组件完整字段和使用

字段

  • refresh 刷新数据
  • api 数据接口
  • resFieldList 数据成功的响应字段
  • pager 是否分页
  • initCurpage 初始化分页
  • data 表格数据
  • fieldList 字段列表
  • handle 操作栏配置
      // 表格相关
      tableInfo: {
        refresh: 1,
        initCurpage: 1,
        data: [],
        fieldList: [{ label: '账号', value: 'account'},
          {label: '用户名', value: 'name'},
          {label: '所属角色', value: 'role_name', minWidth: 120},
          {label: '性别', value: 'sex', width: 80, list: 'sexList'},
          {label: '账号类型', value: 'type', width: 100, list: 'accountTypeList'},
          {label: '状态', value: 'status', width: 90, type: 'slot', list: 'statusList'},
          {label: '创建人', value: 'create_user_name'},
          {label: '创建时间', value: 'create_time', minWidth: 180},
          {label: '更新人', value: 'update_user_name'},
          {label: '更新时间', value: 'update_time', minWidth: 180}
        ],
        handle: {
          fixed: 'right',
          label: '操作',
          width: '280',
          btList: [{ label: '启用', type: 'success', icon: 'el-icon-albb-process', event: 'status', loading: 'statusLoading', show: false, slot: true},
            {label: '编辑', type: '', icon:'el-icon-edit', event:'update', show: false},
            {label: '删除', type: 'danger', icon: 'el-icon-delete', event: 'delete', show: false}
          ]
        }
      }

使用

    <!-- 表格 -->
    <page-table
      :refresh="tableInfo.refresh"
      :init-curpage="tableInfo.initCurpage"
      :data.sync="tableInfo.data"
      :api="getListApi"
      :query="filterInfo.query"
      :field-list="tableInfo.fieldList"
      :list-type-info="listTypeInfo"
      :handle="tableInfo.handle"
      @handleClick="handleClick"
      @handleEvent="handleEvent"
    >
      <!-- 自定义插槽显示状态 -->
      <template v-slot:col-status="scope">
        <i
          :class="scope.row.status === 1 ?'el-icon-check':'el-icon-close'":style="{color: scope.row.status === 1 ? '#67c23a' : '#f56c6c', fontSize: '20px'}"
        />
      </template>
      <!-- 自定义插槽状态按钮 -->
      <template v-slot:bt-status="scope">
        <el-button
          v-if="scope.data.item.show && (!scope.data.item.ifRender || scope.data.item.ifRender(scope.data.row))"
          v-waves
          size="mini"
          :type="scope.data.row.status - 1 >= 0 ?'danger':'success'":icon="scope.data.item.icon":disabled="scope.data.item.disabled":loading="scope.data.row[scope.data.item.loading]"@click="handleClick(scope.data.item.event, scope.data.row)"
        >
          {{scope.data.row.status - 1 >= 0 ? '停用' : '启用'}}
        </el-button>
      </template>
    </page-table>

最后

演示地址

github

退出移动版