如何封裝Vue Element的table表格元件
在封裝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的表格元件是如何通過函式式元件來實現封裝的吧。
效果圖:
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 ? h(Components.Default,{props: {value: empty}}) : h(Components[type],{props: {value,empty,data,...config}}); } }
3、本次封裝將每一列的渲染單獨分開成多個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 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,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,data}}) => { return h('div',{class: 'action'},getAcitons(h,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: '當前監控狀態',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: "通過",filter: ({status}) => status == 'PASS' && this.pass},{label: "駁回",filter: ({status}) => status == 'REJECT' && this.reject},{label: "拒絕",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" ? false : true },download(){ console.log(this.$refs.table.getChecked()) },}; </script> <style> .action span{margin-right:10px;color:#359C67;cursor: pointer;} </style>
2)使用插槽:
<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: '付款金額'},label: '預警統計'},label: '付款時間'},{prop: 'reason',label: '原因'},{prop: 'monitorStatus',{prop: 'customize',label: '自定義展示',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},}),'export'],methods: { query(row){ return { path: '/otherElTable',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 ? v[0] + v[1] + v[2] : '-' } } } </script>
在兩個不太相同的使用方式中,第一種是不基於插槽實現的,第二種是基於插槽實現的。通過兩種方式的對比,可以看出在第二種使用方式中,但凡是使用了插槽的列,在headers陣列中其已經不再定義type欄位了,即使定義了type,它也不起作用,起作用的是插槽,而且也不再使用concat去拼接一個操作列了,操作列也是通過插槽來渲染的,只是如果很多列都通過插槽的形式實現,私心覺得頁面看起來就不那麼整潔了。
多說一句,既然我們已經對大部分場景的實現進行了封裝,那麼大家在使用時就沒必要再通過插槽的形式去多此一舉了,儘量保持頁面的整潔。如果你實在覺得在headers陣列後邊再concat一個操作列的方式有點彆扭,那麼就只需將操作列通過插槽的形式去實現就OK了。本部落格中提到的插槽實現形式,只是為了給大家多一種選擇而已。
最後,關於金額千分位和時間戳格式化的實現,這裡就不再貼程式碼了,可自行實現。
最近又想了一下封裝的這個table元件,想著說在原來封裝的基礎上還有沒有其他的實現方法,比如我不想在原來定義的headers陣列後邊再concat一個操作列,再比如表格的某一列的資料處理方法不包含在我們之前所封裝的那些方法當中,或者說作為第一次使用這個table元件的前端開發人員,我不太習慣你的那種寫法,那我可不可以在你封裝的基礎上自己寫一些處理方法呢,答案是可以的,當然我們說既然已經封裝好了元件,那麼大家就按照一個套路來,省時又省力,何樂而不為呢?但有一說一,我們本著學習的態度,本著藝多不壓身的出發點來看的話,多學多思考多動手,總歸是有益於進步的。只是在實際的開發過程中,我們儘量要選擇一種封裝方式,然後大家一起遵守這個約定就好了。
其實說了這麼多廢話,這次變更也是沒有多大力度的,只是在原來封裝的基礎上增加了插槽而已。看過本篇部落格的你一定還記得我封裝的程式碼中有一段專門用來處理每一列資料的程式碼吧:
<Cell :config="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',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',type: "Date"},
寫到這裡,其實我想說,即使加上了插槽,那麼對之前的那些使用方法來說,基本沒啥影響,你該怎麼用還怎麼用,我只是給你提供了更多的選擇而已。
如果你實在不想用插槽,想保持頁面的整潔,那你在<Cell :config="n" :data="row" />這段程式碼的外面包裹不包裹一層插槽都無所謂,直接使用上文中我介紹的第一種使用方法就可以了。
作者:小壞
出處:http://tnnyang.cnblogs.com
以上就是如何封裝Vue Element的table表格元件的詳細內容,更多關於封裝Vue Element的table表格元件的資料請關注我們其它相關文章!