封裝Vue Element的table表格元件

小壞先生發表於2020-08-18

上週分享了幾篇關於React元件封裝方面的博文,這周就來分享幾篇關於Vue元件封裝方面的博文,也好讓大家能更好地瞭解React和Vue在元件封裝方面的區別。

在封裝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元件,還可以是一個返回DOM節點字串或Vue元件的函式;第二個引數是一個物件,這個引數是可選的,定義了渲染元件所需的引數;第三個引數是子級虛擬節點,可以是一個由createElement函式建立的元件,也可以是一個普通的字串如:'hello world',還可以是一個陣列,當然也可以是一個返回DOM節點字串或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的表格元件是如何通過函式式元件來實現封裝的吧。

效果圖:

1、所封裝的table元件

<template>
  <el-table :data="config.tableData" style="width: 100%" v-on="cfg.on" v-bind="cfg.attrs">
    <el-table-column v-if="cfg.hasCheckbox" type="selection" width="55" label="xx" />
    <el-table-column v-for="n in cfg.headers" :prop="n.prop" :label="n.name" :key="n.prop" v-bind="{...columnAttrs, ...n.attrs}">
      <template slot-scope="{row}">
        <Cell :config="n" :data="row" />
      </template>
    </el-table-column>
  </el-table>
</template>

<script>
import Cell from './cell'

export default {
  components: {
    Cell,
  },
  props: {
    config: Object,
  },
  data(){
    return {
      columnAttrs: {
        align: 'left',
        resizable: false,
      },
      cfg: {
        on: this.getTableEvents(),
        attrs: {
          border: true,
          stripe: true,
        },
        ...this.config,
      },
      checked: [],
    }
  },
  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;
    },
  },
}
</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 ? h(Components.Default, {props: {value: empty}}) : h(Components[type], {props: {value, empty, data, ...config}});
  }
}

3、不同於封裝React AntD的table表格元件時將表格的每一列的渲染都集中在了一個table.js中,本次封裝將每一列的渲染單獨分開成多個vue元件,最後再合併在一個components.js檔案中一起進行匹配。

1)整合檔案components.js:

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 Loop         from './Loop';
import Popover      from './Popover';

export {
  Default,
  Date,
  Currency,
  Enum,
  Action,
  Link,
  Loop,
  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 ? empty : _value);

        let {id, val} = _value;
        return h('span', {staticClass: id}, [h('span', val)]);
    }
}

6)操作列Action.js

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 functional>
  <router-link :to="{ path: props.url, query: {id: props.data.id} }">{{props.value}}</router-link>
</template>

8)可迴圈展示陣列資料的列Loop.vue

<template functional>
  <div v-html="props.Loop(props.value)" />
</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元件:

<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', name: '業務編號', attrs: {width: 200, align: 'center'}},
          {prop: 'payeeAcctName', name: '收款賬戶名', type: 'Link', url: 'otherElTable', attrs: {width: 260, align: 'right'}},
          {prop: 'tradeAmt', name: '付款金額', type: 'Currency'},
          {prop: 'status', name: '操作狀態', type: 'Enum', Enum: {name: 'order'}},
          {prop: 'statistic', name: '預警統計', type: 'Loop', Loop: (val) => this.loop(val)},
          {prop: 'reason', name: '原因', type: 'Popover'},
          {prop: 'payTime', name: '付款時間', type: "Date", format: 'yyyy-MM-dd hh:mm:ss'},   //不設定format的話,日期格式預設為yyyy/MM/dd
          {prop: 'monitorStatus', name: '當前監控狀態', type: 'Enum', Enum: {name: 'monitor'}},
        ].concat(this.getActions()),
        tableData: [
          {id: 1, contractCode: '', payeeAcctName: '中國銀行上海分行', tradeAmt: '503869', status: '00', payTime: 1593585652530, 
            statistic:[
              {level: 3, total: 5},
              {level: 2, total: 7},
              {level: 1, total: 20},
              {level: 0, total: 0}
            ]
          },
          {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},
        ],
        hasCheckbox: true,
      },
      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},
      ]}
    },
    loop(val){
      let str = '';
      val.forEach(t => {
        str += '<span style="margin-right:5px;">' + t.total + '</span>';
      })
      return str;
    },
    download(){
      console.log(this.$refs.table.getChecked())
    },
  },
};
</script>
<style>
.action span{margin-right:10px;color:#359C67;cursor: pointer;}
</style>

關於金額千分位和時間戳格式化的實現,這裡就不再貼程式碼了,可自行實現。

相關文章