如何封装Vue Element的table表格组件

(编辑:jimmy 日期: 2024/12/22 浏览:2)

在封装Vue组件时,我依旧会交叉使用函数式组件的方式来实现。关于函数式组件,我们可以把它想像成组件里的一个函数,入参是渲染上下文(render context),返回值是渲染好的HTML(VNode)。它比较适用于外层组件仅仅是对内层组件的一次逻辑封装,而渲染的模板结构变化扩展不多的情况,且它一定是无状态、无实例的,无状态就意味着它没有created、mounted、updated等Vue的生命周期函数,无实例就意味着它没有响应式数据data和this上下文。

我们先来一个简单的Vue函数式组件的例子吧,然后照着这个例子来详细介绍一下。

export default {
 functional: true,
 props: {},
 render(createElement, context) {
   return createElement('span', 'hello world')
 }
}

Vue提供了一个functional开关,设置为true后,就可以让组件变为无状态、无实例的函数式组件。因为只是函数,所以渲染的开销相对来说较小。

函数式组件中的Render函数,提供了两个参数createElement和context,我们先来了解下第一个参数createElement。

createElement说白了就是用来创建虚拟DOM节点VNode的。它接收三个参数,第一个参数可以是DOM节点字符串,也可以是一个Vue组件,还可以是一个返回字符串或Vue组件的函数;第二个参数是一个对象,这个参数是可选的,定义了渲染组件所需的参数;第三个参数是子级虚拟节点,可以是一个由createElement函数创建的组件,也可以是一个普通的字符串如:'hello world',还可以是一个数组,当然也可以是一个返回字符串或Vue组件的函数。

createElement有几点需要注意:

  • createElement第一个参数若是组件,则第三个参数可省略,即使写上去也无效;
  • render函数在on事件中可监听组件$emit发出的事件
  • 在2.3.0之前的版本中,如果一个函数式组件想要接收prop,则props选项是必须的。在2.3.0或以上的版本中,你可以省略props选项,组件上所有的attribute都会被自动隐式解析为prop。

函数式组件中Render的第二个参数是context上下文,data、props、slots、children以及parent都可以通过context来访问。

在2.5.0及以上版本中,如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:<template functional></template>, 但是如果Vue组件中的render函数存在,则Vue构造函数不会从template选项或通过el选项指定的挂载元素中提取出的HTML模板编译渲染函数,也就是说一个组件中templete和render函数不能共存,如果一个组件中有了templete,即使有了render函数,render函数也不会执行,因为template选项的优先级比render选项的优先级高。

到这里,Vue函数式组件介绍的就差不多了,我们就来看看Element的表格组件是如何通过函数式组件来实现封装的吧。

效果图:

如何封装Vue Element的table表格组件

1、所封装的table组件:

<template>
 <div>
  <el-table :data="cfg.data" style="width: 100%" v-on="cfg.on" v-bind="attrs" v-loading="loading">
   <el-table-column v-if="cfg.hasCheckbox" v-bind="selectionAttrs" type="selection" width="55" label="xx" />
   <el-table-column v-for="n in cfg.headers" :prop="n.prop" :label="n.label" :key="n.prop" v-bind="{...columnAttrs, ...n.attrs}">
    <template slot-scope="{row}">
     <slot :name="n.prop" :row="row"><Cell :config="n" :data="row" /></slot>
    </template>
   </el-table-column>
  </el-table>
  <el-pagination
   class="pagination"
   v-if="showPage"
   layout="total, sizes, prev, pager, next, jumper"
   :page-sizes="[2, 3, 6, 11]"
   :page-size="page.size"
   :total="page.total"
   :current-page="page.page"
   @current-change="loadPage"
   @size-change="sizeChange"
  />
 </div>
</template>

<script>
import Cell from './cell'

export default {
 components: {
  Cell,
 },
 props: {
  config: Object,
 },
 data(){
  return {
   loading: true,
   columnAttrs: {
    align: 'left',
    resizable: false,
   },
   cfg: {
    on: this.getTableEvents(),
    attrs: {
     border: true,
		   stripe: true,
    },
    data: [],
    ...this.config,
   },
   page: {
    size: this.config.size || 10,
    page: 1,
    total: 0,
   },
   checked: [],
  }
 },
 created(){
  this.load();
 },
 computed: {
  selectionAttrs(){
   let {selectable, reserveSelection = false} = this.config || {}, obj = {};
   // checkBox是否可以被选中
   if(selectable && typeof selectable == 'function'){
    Object.assign(obj, {
     selectable,
    })
   }
   //reserve-selection仅对type=selection的列有效,类型为Boolean,为true则会在数据更新之后保留之前选中的数据(需指定 row-key)
   if(reserveSelection){
    Object.assign(obj, {
     'reserve-selection': reserveSelection,
    })
   }

   return obj;
  },
  attrs(){
   let {config: {spanMethod, rowKey}, cfg: {attrs}} = this;
   // 合并单元格 - spanMethod是父组件传过来的合并单元格的方法,请参照element合并单元格
   if(spanMethod && typeof spanMethod == 'function'){
    Object.assign(attrs, {
     'span-method': spanMethod,
    })
   }
   // 表格跨页选中,需要设置row-key和reserve-selection,reserve-selection只能且必须设置在type为selection的el-table-column上
   if(rowKey && typeof rowKey == 'function'){
    Object.assign(attrs, {
     'row-key': rowKey,
    })
   }

   return attrs;
  },
  showPage(){
   let {size, total} = this.page;
   return size < total;
  },
 },
 methods: {
  getTableEvents(){
   let {hasCheckbox = false} = this.config || {}, events = {}, _this = this;
   if(hasCheckbox){
    // 绑定事件
    Object.assign(events, {
     'selection-change'(v){
     	_this.checked = v;
     },
    });
   }

   return events;
  },
  // 获取勾选的行
  getChecked(){
   return this.checked;
  },
  // 请求数据
  load(p = {}){
   let { size, page } = this.page, {loadData = () => Promise.resolve({})} = this.config;
   this.loading = true;
   // 这里loadData的参数在初始化时只有分页所需的page和size,至于接口需要的其他参数,是在父组件的loadData中传递
   loadData({...p, page, size}).then(({data, total}) => {
    this.cfg.data = data;
    this.page.page = page;
    this.page.total = total;
    this.loading = false;
   });
  },
  loadPage(index){
   this.page.page = index
   this.load();
  },
  sizeChange(size){
   this.page.size = size
   this.load();
  },
  // 一般在点击查询按钮或局部刷新表格列表时,可调用此方法,如果不传参数,则默认从第一页开始
  reload(p = {}){
   this.page.page = 1
   this.load(p);
  },
 },
}
</script>

2、汇总表格每一列的cell.js:

import * as Components from './components';
let empty = '-'
export default {
 props: {
  config: Object,
  data: Object,
 },
 functional: true,
 render: (h, c) => {
  let {props: {config = {}, data = {}}} = c, {prop, type = 'Default'} = config, value = data[prop] || config.value, isEmpty = value === '' || value === undefined;
  return isEmpty "htmlcode">
import Date     from './Date';
import Default   from './Default';
import Currency   from './Currency';
import Enum     from './Enum';
import Action    from './Action';
import Link     from './Link';
import Format    from './Format';
import Popover   from './Popover';

export {
 Default,
 Date,
 Currency,
 Enum,
 Action,
 Link,
 Format,
 Popover,
}

2)日期列Date.vue

<template functional>
  <span>{{props.value | date(props.format)}}</span>
</template>

3)默认列Default.vue

<template functional>
  <span>{{props.value}}</span>
</template>

4)金额千分位列Currency.vue

<template functional>
  <span>{{props.value | currency}}</span>
</template>

5)映射列Enum.js

let mapIdAndKey = list => list.reduce((c, i) => ({...c, [i.key]: i}), {});

let STATUS = {
  order: mapIdAndKey([
    {
      id: 'draft',
      key: 'CREATED',
      val: '未提交',
    },
    {
      id: 'pending',
      key: 'IN_APPROVAL',
      val: '审批中',
    },
    {
      id: 'reject',
      key: 'REJECT',
      val: '审批驳回',
    },
    {
      id: 'refuse',
      key: 'REFUSE',
      val: '审批拒绝',
    },
    {
      id: 'sign',
      key: 'CONTRACT_IN_SIGN',
      val: '合同签署中',
    },
    {
      id: 'signDone',
      key: 'CONTRACT_SIGNED',
      val: '合同签署成功',
    },
    {
      id: 'lendDone',
      key: 'LENDED',
      val: '放款成功',
    },
    {
      id: 'lendReject',
      key: 'LOAN_REJECT',
      val: '放款驳回',
    },
    {
      id: 'cancel',
      key: 'CANCEL',
      val: '取消成功',
    },
    {
      id: 'inLend',
      key: 'IN_LOAN',
      val: '放款审批中',
    },
  ]),
  monitor: mapIdAndKey([
    {
      key: '00',
      val: '未监控',
    },
    {
      key: '01',
      val: '监控中',
    },
  ]),
}

export default {
  functional: true,
  render(h, {props: {value, Enum, empty}, parent}){
    let enums = Object.assign({}, STATUS, parent.$store.getters.dictionary),
      {name = '', getVal = (values, v) => values[v]} = Enum, _value = getVal(enums[name], value);
      
    if( _value === undefined) return h('span', _value === undefined "htmlcode">
const getAcitons = (h, value, data) => {
 let result = value.filter(n => {
  let {filter = () => true} = n;
  return filter.call(n, data);
 });

 return result.map(a => h('span', {class: 'btn', on: {click: () => a.click(data)}, key: a.prop}, a.label))
}

export default {
 functional: true,
 render: (h, {props: {value, data}}) => {
  return h('div', {class: 'action'}, getAcitons(h, value, data))
 },
}

7)带有可跳转链接的列Link.vue

<template>
 <router-link :to="{ path, query: params }">{{value}}</router-link>
</template>

<script>
export default {
 props: {
  data: Object,
  value: String,
  query: {
   type: Function,
   default: () => {
    return {
     path: '',
     payload: {}
    }
   }
  },
 },
 computed: {
  // 路由path
  path(){
   const { path } = this.query(this.data)
   return path
  },
  params(){
   const { payload } = this.query(this.data)
   return payload
  },
 },
}
</script>

8)自定义想要展示的数据格式Format.vue

<template functional>
 <div v-html="props.format(props.value, props.data)" />
</template>

9)当内容过多需要省略并在鼠标移入后弹出一个提示窗显示全部内容的列Popover.vue

<template functional>
 <el-popover
  placement="top-start"
  width="300"
  trigger="hover"
  popper-class="popover"
  :content="props.value">
  <span slot="reference" class="popover-txt">{{props.value}}</span>
 </el-popover>
</template>
<style scoped>
.popover-txt{
 overflow:hidden;
 text-overflow:ellipsis;
 white-space:nowrap;
 display: block;
 cursor: pointer;
}
</style>

从以上代码中可以看出,我既使用了基于render函数类型的函数式组件也使用了基于模板的函数式组件,主要是为了在封装时的方便,毕竟使用render这个最接近编译器的函数还是有点麻烦的,不如基于模板的函数式组件来的方便。

4、使用封装后的表格table组件

1)不使用插槽:

<template>
 <div style="margin: 20px;">
  <el-button type="primary" v-if="excelExport" @click="download">获取勾选的表格数据</el-button>
  <Table :config="config" ref="table" />
 </div>
</template>

<script>
import Table from '@/components/table'

export default {
 components: {
  Table,
 },
 data() {
  return {
   config: {
    headers: [
     {prop: 'contractCode', label: '业务编号', attrs: {width: 200, align: 'center'}},
     {prop: 'payeeAcctName', label: '收款账户名', type: 'Link', query: row => this.query(row), attrs: {width: 260, align: 'right'}},
     {prop: 'tradeAmt', label: '付款金额', type: 'Currency'},
     {prop: 'status', label: '操作状态', type: 'Enum', Enum: {name: 'order'}},
     {prop: 'statistic', label: '预警统计', type: 'Format', format: val => this.format(val)},  //自定义展示自己想要的数据格式
     {prop: 'reason', label: '原因', type: 'Popover'},
     {prop: 'payTime', label: '付款时间', type: "Date", format: 'yyyy-MM-dd hh:mm:ss'},  //不设置format的话,日期格式默认为yyyy/MM/dd
     {prop: 'monitorStatus', label: '当前监控状态', type: 'Enum', Enum: {name: 'monitor'}},
    ].concat(this.getActions()),
    //通过接口获取列表数据 - 这里的参数p就是子组件传过来的包含分页的参数
    loadData: p => request.post('permission/list', {...this.setParams(), ...p}),
    hasCheckbox: true,
    selectable: this.selectable,
    reserveSelection: false,
    rowKey: row => row.id,
   },
   status: "01",
   permission: ["handle", "pass", "refuse", "reApply", 'export']
  }
 },
 computed: {
  handle() {
   return this.permission.some(n => n == "handle");
  },
  pass() {
   return this.permission.some(n => n == "pass");
  },
  reject() {
   return this.permission.some(n => n == "reject");
  },
  refuse() {
   return this.permission.some(n => n == "refuse");
  },
  excelExport(){
   return this.permission.some(n => n == "handle") && this.permission.some(n => n == "export");
  },
 },
 methods: {
  getActions(){
   return {prop: 'action', name: '操作', type: "Action", value: [
    {label: "查看", click: data => {console.log(data)}},
    {label: "办理", click: data => {}, filter: ({status}) => status == 'CREATED' && this.handle},
    {label: "通过", click: data => {}, filter: ({status}) => status == 'PASS' && this.pass},
    {label: "驳回", click: data => {}, filter: ({status}) => status == 'REJECT' && this.reject},
    {label: "拒绝", click: data => {}, filter: ({status}) => status == 'CREATED' && this.refuse},
   ]}
  },
  setParams(){
   return {
    name: '测试',
    status: '01',
    type: 'CREATED',
   }
  },
  query(row){
   return {
    path: '/otherElTable', // 路由path
    payload: {
     id: row.id,
     type: 'link'
    }
   }
  },
  format(val){
   let str = '';
   val.forEach(t => {
    str += '<span style="margin-right:5px;">' + t.total + '</span>';
   })
   return str;
  },
  selectable({status}){
   return status == "REFUSE" "htmlcode">
 <Table :config="config" ref="table">
  <template #statistic="{row}">
   <div v-html="loop(row.statistic)"></div>
  </template>
  <template #payeeAcctName="{row}">
   {{row.payeeAcctName}}
  </template>
  <template #tradeAmt="{row}">
   {{row.tradeAmt | currency}}
  </template>
  <template v-slot:reason="{row}">
   <template v-if="!row.reason">-</template>
   <el-popover
    v-else
    placement="top-start"
    width="300"
    trigger="hover"
    popper-class="popover"
    :content="row.reason">
    <span slot="reference" class="popover-txt">{{row.reason}}</span>
   </el-popover>
  </template>
  <template #payTime="{row}">
   {{row.payTime | date('yyyy-MM-dd hh:mm:ss')}}
  </template>
  <template #customize="{row}">
   {{customize(row.customize)}}
  </template>
  <template #opt="{row}">
   <div class="action">
    <span>查看</span>
    <span v-if="row.status == 'CREATED' && handle">办理</span>
    <span v-if="row.status == 'PASS' && pass">通过</span>
    <span v-if="row.status == 'REJECT' && reject">驳回</span>
    <span v-if="row.status == 'REFUSE' && refuse">拒绝</span>
   </div>
  </template>
 </Table>

<script>
import Table from '@/components/table'

export default {
 components: {
  Table,
 },
 data(){
  return {
   config: {
    headers: [
     {prop: 'contractCode', label: '业务编号', attrs: {width: 200, align: 'center'}},
     {prop: 'payeeAcctName', label: '收款账户名', attrs: {width: 260, align: 'right'}},
     {prop: 'tradeAmt', label: '付款金额'},
     {prop: 'status', label: '操作状态', type: 'Enum', Enum: {name: 'order'}},
     {prop: 'statistic', label: '预警统计'},
     {prop: 'payTime', label: '付款时间'},
     {prop: 'reason', label: '原因'},
     {prop: 'monitorStatus', label: '当前监控状态', type: 'Enum', Enum: {name: 'monitor'}},
     {prop: 'customize', label: '自定义展示', type: 'Format', format: val => this.customize(val)},
     {prop: 'opt', label: '操作'},
    ],
    loadData: () => Promise.resolve({
     data: [
      {id: 1, contractCode: '', payeeAcctName: '中国银行上海分行', tradeAmt: '503869.265', status: '00', payTime: 1593585652530,
       statistic:[
        {level: 3, total: 5},
        {level: 2, total: 7},
        {level: 1, total: 20},
        {level: 0, total: 0}
       ],
       customize: ['中国', '上海', '浦东新区']
      },
      {id: 2, contractCode: 'GLP-YG-B3-1111', payeeAcctName: '中国邮政上海分行', tradeAmt: '78956.85', status: 'CREATED', payTime: 1593416718317, 
       reason: 'Popover的属性与Tooltip很类似,它们都是基于Vue-popper开发的,因此对于重复属性,请参考Tooltip的文档,在此文档中不做详尽解释。',
      },
      {id: 3, contractCode: 'HT1592985730310', payeeAcctName: '招商银行上海支行', tradeAmt: '963587123', status: 'PASS', payTime: 1593420950772, monitorStatus: '01'},
      {id: 4, contractCode: 'pi239', payeeAcctName: '广州物流有限公司', tradeAmt: '875123966', status: 'REJECT', payTime: 1593496609363},
      {id: 5, contractCode: '0701001', payeeAcctName: '建设银行上海分账', tradeAmt: '125879125', status: 'REFUSE', payTime: 1593585489177},
     ],
    }),
   },
   permission: ["handle", "pass", "refuse", "reApply", 'export'],
  }
 },
 computed: {
  handle() {
   return this.permission.some(n => n == "handle");
  },
  pass() {
   return this.permission.some(n => n == "pass");
  },
  reject() {
   return this.permission.some(n => n == "reject");
  },
  refuse() {
   return this.permission.some(n => n == "refuse");
  },
  excelExport(){
   return this.permission.some(n => n == "handle") && this.permission.some(n => n == "export");
  },
 },
 methods: {
  query(row){
   return {
    path: '/otherElTable',  // 路由path
    payload: {
     id: row.id,
     type: 'link'
    }
   }
  },
  loop(val){
   if(!val) return '-'
   let str = '';
   val.forEach(t => {
    str += '<span style="margin-right:5px;">' + t.total + '</span>';
   })
   return str;
  },
  customize(v){
   return v "n" :data="row" />
对,就是它。对于它,我不想再多说了,上边已经做了介绍了。本次变更,我们主要用到的是插槽。

插槽这个API,VUE的官网和网上的各种文章介绍已经讲的很清楚了,它大概分为:默认插槽(也有人管它叫匿名插槽)、具名插槽和作用域插槽。关于它们的介绍,请自行查阅官网或网上的各种文章资料。本次变更主要用到的就是具名插槽和作用域插槽。具名插槽,顾名思义就是带有名称的插槽,我们本次封装所使用的插槽的名称来自于table的每一列的prop。作用域插槽在本次封装中的作用主要就是通过子组件的插槽向父组件传值,其实现形式有点类似于vue父组件向子组件传值,只不过两者的接收值的方式不同。总之此次变更实现起来还是很简单的,就是在<Cell :config="n" :data="row" />的外边再包一层具名插槽就可以了。
<slot :name="n.prop" :row="row"><Cell :config="n" :data="row" /></slot>
就酱。

接下来,我们就可以回答上边我们提出的那些问题了。来看答案:

<Table :config="config" ref="table">
  <template #payTime="{row}">
   {{row.payTime | date('yyyy-MM-dd hh:mm:ss')}}
  </template>
  <template #customize="{row}">
   {{customize(row.customize)}}
  </template>
  <template #opt="{row}">
   <div class="action">
    <span>查看</span>
    <span v-if="row.status == 'CREATED' && handle">办理</span>
    <span v-if="row.status == 'PASS' && pass">通过</span>
    <span v-if="row.status == 'REJECT' && reject">驳回</span>
    <span v-if="row.status == 'REFUSE' && refuse">拒绝</span>
   </div>
  </template>
</Table>

以上就是对某些特殊情况,而你又不想使用我最开始封装的那些方法来实现,那么可以,我就再为你提供一个其他的“特殊服务”。这里要注意,如果你使用插槽来自己渲染数据,那么在headers数组中,你需要提供表格头部的渲染,而不需要再加入type字段即可。
比如最开始渲染表格的日期列时我们是这么写的:
{prop: 'payTime', label: '付款时间', type: "Date", format: 'yyyy-MM-dd hh:mm:ss'}
那么如果你使用插槽来自己渲染数据,这里的写法就要变成了这样:
{prop: 'payTime', label: '付款时间'}
还有之前我们定义操作列是在headers数组的后边再concat了一个数组,如果你使用插槽来自己渲染数据,那么就不需要再concat一个数组了,而是在headers数组中再加一个{prop: 'opt', label: '操作'}就可以了。

其实,这次变更说的是在原来的基础上重新包装了一层插槽,那么对于那些不需要我们自行处理数据,只需直接展示接口返回的数据的情况,我们在使用这个封装的table组件时也不需要进行什么特殊处理,更不需要像上边使用插槽那样去定义,只要还是跟之前一样在headers数组中正常定义就可以了。因为插槽嘛,你不定义具名插槽,也不定义默认插槽,那么插槽中显示的就是包裹在插槽标签slot中的<Cell :config="n" :data="row" />
明白了吧。

多说一句,你说我不想使用插槽去处理日期、金额千分位这些列,那么你依旧可以根据上边我介绍的插槽的原理,在headers数组中依旧这样定义就OK了:

{prop: 'tradeAmt', label: '付款金额', type: 'Currency'},
{prop: 'payTime', label: '付款时间', type: "Date"},

写到这里,其实我想说,即使加上了插槽,那么对之前的那些使用方法来说,基本没啥影响,你该怎么用还怎么用,我只是给你提供了更多的选择而已。

如果你实在不想用插槽,想保持页面的整洁,那你在<Cell :config="n" :data="row" />这段代码的外面包裹不包裹一层插槽都无所谓,直接使用上文中我介绍的第一种使用方法就可以了。

作者:小坏

出处:http://tnnyang.cnblogs.com

以上就是如何封装Vue Element的table表格组件的详细内容,更多关于封装Vue Element的table表格组件的资料请关注其它相关文章!

一句话新闻

Windows上运行安卓你用过了吗
在去年的5月23日,借助Intel Bridge Technology以及Intel Celadon两项技术的驱动,Intel为PC用户带来了Android On Windows(AOW)平台,并携手国内软件公司腾讯共同推出了腾讯应用宝电脑版,将Windows与安卓两大生态进行了融合,PC的使用体验随即被带入到了一个全新的阶段。