iview可編輯表格元件封裝
因為公司需要嘗試新的UI框架,因此自己也是學習了iview這個新的框架,之前一直都是用的element-ui,正好公司專案用到可編輯表格這樣的元件,但是網上也是搜不到相關的資料,所以自己在參考了iview-admin的一些方法之後,自己寫了一個,當然這個元件還是有一些缺陷後面會說到,本篇就是記錄一下自己學習新的ui框架的一個過程。
首先自己希望的可編輯的表格是這樣的一種形式。
就是這樣的整行可以點選編輯的樣式,看似好像很簡單,但是其中有非常多的坑。首先按照自己的想法,就是講表格中顯示編輯和實際資料的地方抽象出來成為一個元件
// inputEdit.vue <template> <div> <div v-if="params.column.options === 'handle'"> <div v-show="editIndex !== params.index" class="handle"> <Button @click="startEdit" type="default" size="small">編輯</Button> <Poptip confirm title="確認刪除" @on-ok="deleteData"> <Button type="default" size="small">刪除</Button> </Poptip> </div> <div v-show="editIndex === params.index" class="handle"> <Button @click="saveEdit" type="default" size="small">儲存</Button> <Button @click="cancelEdit" type="default" size="small">取消</Button> </div> </div> <div class="tables-edit-outer" v-else> <div class="tables-edit-con" v-show="editIndex !== params.index"> <span class="value-con">{{value}}</span> </div> <div class="tables-editting-con" v-show="editIndex === params.index"> <Input v-show="params.column.options === 'input'" v-model="params.row[params.column.key]" @on-change="handleChange" :maxlength='25' placeholder="請輸入內容"> </Input> <Select v-show="params.column.options === 'select'" v-model="params.row[params.column.key]" @on-change="handleChange"> <Option v-for="(item, index) in options" :key="index" :value="item.label"> {{item.value}} </Option> </Select> <DatePicker v-show="params.column.options === 'date'" placeholder="請選擇年份" type="year" :editable="editable" :value="String(params.row[params.column.key])" @on-change="handleChange" > </DatePicker> <DatePicker v-show="params.column.options === 'dateMonth'" placeholder="請選擇年月" type="month" :editable="editable" :value="String(params.row[params.column.key])" @on-change="handleChange" > </DatePicker> <DatePicker v-show="params.column.options === 'dateAll'" placeholder="請選擇日期" type="date" :editable="editable" :value="String(params.row[params.column.key])" @on-change="handleChange" > </DatePicker> </div> </div> </div> </template> <script> export default { name: 'TablesEdit', data () { return { editRow: {}, editable: false } }, props: { value: {required: true}, params: Object, tableData: Array, options: Array, columns: Array, editIndex: [Number,String], backupsRow: Object }, methods: { handleChange (e) { if (this.params.column.options === 'date' || this.params.column.options === 'dateMonth' || this.params.column.options === 'dateAll' || this.params.column.options === 'select') { this.params.row[this.params.column.key] = e return; } this.params.row[this.params.column.key] = e.target.value this.$emit('on-editing',this.params) }, deleteData () { this.$emit('on-delete', this.params) }, startEdit () { this.$emit('on-start-edit', this.params) this.editRow = this.params.row }, saveEdit () { this.$emit('on-save-edit', this.params) this.editRow = {} }, cancelEdit () { this.$emit('on-cancel-edit',this.params) } } } </script>
樣式這裡就忽略了,可以看到邏輯還是很簡單的,就是根據不同的列選項,來展示相應的輸入編輯框,可能很多同學看了覺得這個日期框可以複用,不用一個一個判斷,當時也是這麼想的,但是實現的時候發現,不同型別的日期框,顯示的值也是不一樣的,如果複用這個的話,父級元件上面的prop非常多,不利於這個元件的複用,而這個編輯元件其實內容還是比較少的。 搞定了這個以後,再來看整個表的結構,我們需要把這個元件放在一個表格的元件中,所以有了這樣,
// tableEdit.vue <div class="my-edit-table"> <Table ref="tablesMain" border :data="insideTableData" :columns="insideColumns" > <slot name="header" slot="header"></slot> <slot name="footer" slot="footer"> <Table v-if="showSummary" :show-header="false" :data="summaryData" :columns="summaryColumns"></Table> </slot> <slot name="loading" slot="loading"></slot> </Table> <div class="add"> <Button type="default" style="width: 100%" icon="md-add" @click='addRow'>新增</Button> </div> </div>
博主這裡是增加了一個新增行的功能,邏輯其實就是在表格資料上新增一行空資料,另外因為iview沒有合計行的功能,所以博主自己也是寫了一個類似element的合計行,看到這裡其實我們剛剛的元件都還沒有用上,彆著急,其實iview的表格元件和element表格最大的區別就在於自定義方法的不同,iview是用了vue的render函式來構造的表格自定義的,而element則是將自定義的dom元素封裝成了模板,拿來用就可以了,所以相比較而言,iview還是要複雜一些的。來看看我們是怎麼使用我們剛剛的元件的。
// 動態渲染表格的每列的render函式 suportEdit (item, index) { item.render = (h, params) => { return h(TablesEdit, { props: { params: params, value: this.insideTableData[params.index][params.column.key], tableData: this.insideTableData, options: this.options, editIndex: this.editIndex, columns: this.insideColumns, backupsRow: this.backups }, on: { 'on-editing': params => { this.tempSummary.splice(params.index, 1, params.row) }, 'on-delete': params => { this.insideTableData.splice(params.index, 1) this.tempSummary.splice(params.index, 1) if (params.row.id) { this.deleteData.push(params.row) this.$emit('delete-data',this.deleteData) } else { this.addData.splice(params.index, 1) this.$emit('add-data',this.addData) } }, 'on-cancel-edit': (params) => { this.editIndex = -1; if (this.isAddRow) { this.insideTableData.splice(params.index, 1) this.isAddRow = false; } else { this.insideTableData.splice(params.index, 1, this.backups) this.tempSummary = this.insideTableData.slice() } this.backups = {}; this.$emit('get-save'); }, 'on-start-edit': (params) => { if (this.editIndex !== -1) { this.$Message.warning({content: '請先儲存資料'}) return; } this.backups = Object.assign({}, params.row) this.editIndex = params.index this.$emit('not-save') }, 'on-save-edit': (params) => { // 驗證輸入 let identity = this.identity.bind(this, params) if (identity()) { this.insideTableData[params.index] = params.row if (params.row.id) { let flag = false this.updateData.forEach(item => { if (item.id === params.row.id) flag = true }) // 設定標識,如果更新集合中的某項id等於當前編輯行的id,則表示資料並未更新,因此不新增到更新陣列 if (!flag) { this.updateData.unshift(params.row) } this.$emit('update-data',this.updateData) } this.editIndex = -1; if (this.isAddRow) { this.addData.push(params.row) this.$emit('add-data',this.addData) this.isAddRow = false } else if (!params.row.id) { this.addData.splice(-1, 1, params.row) this.$emit('add-data',this.addData) } this.backups = {} this.$emit('get-save') } } } }) } return item }, // 根據父元件傳過來的列資料新增渲染函式 handleColumns (columns) { this.insideColumns = columns.map((item, index) => { let res = item if (res.children) { let child = res.children child.forEach((item, index) => { item = this.suportEdit(item, index) }) } else { res = this.suportEdit(res, index) } return res }) },
整個邏輯其實很簡單,只是非常複雜,稍不注意就可能產生了意向不到的bug,首先我們需要將父元件傳過來的表格列的陣列進行遍歷,因為考慮到複雜表頭的情況,所以我們需要深層去遍歷,遍歷以後,每一項執行suportEdit函式,這個函式其實就是iview當中封裝的渲染函式,注意這個函式和原生的vue的渲染函式又有些不一樣,上面的程式碼中可以看到,render函式有兩個引數,一個就是我們熟悉的h渲染函式,另外一個則是一個物件params,這個params其實就是表格中的一些資料,有行資料,列資料,以及索引等等。其次我們通過this.$emit的方法,來向父元件傳遞一些自定義的事件,比如開始編輯,刪除等按鈕的事件,這個也是博主感覺不好的地方,因為資料流過於混亂,有傳入子元件的資料,也有傳入父元件的資料,並不是vue中倡導的單向資料流,後續維護可能會比較麻煩,因為追蹤這些資料的流向可能就是一個比較大的工程,而博主這樣做,也是因為公司專案中這樣的表格非常多,如果每次都重複這樣的元件的話,在對效能影響不多的情況,還是選擇將這個元件抽象出來更好,所以如果你的專案中這樣的表格並不多,還是iview每個列渲染一次就可以了。
tableEdit元件才是我們需要複用的元件,注意這裡用了一些addData,updateData,deleteData這樣的陣列,其實就是編輯過程中,更改,增加以及刪除的資料,最終是要傳給服務端的,所以博主將這些資料抽離出來,最終當做引數傳遞給服務端,當然如果你只需要傳入最終的表格資料作為引數的話,就不需要這樣的一些資料。
這個元件最重要的地方,其實就是四個按鈕,編輯,刪除,儲存,取消,這四個按鈕邏輯看似簡單,但是其實還是比較複雜的,首先點選編輯話,改變編輯狀態editIndex,這樣切換到編輯狀態,同時輸入資料修改的時候,要將編輯前的資料儲存到臨時資料backupRow中,因為每次編輯的值會傳給v-model繫結的值,這樣表格資料的值也會被改變了,本來沒有問題,但是如果你點選了取消按鈕,編輯的資料無效,但是還是改變了表格資料不就矛盾了嗎,其次,當點選刪除的時候,則將點選按鈕的索引傳遞給表格陣列,刪除掉索引行的表格資料來實現刪除,這個還是比較簡單的,第三則是點選儲存的時候,因為這裡記錄了update更新的陣列,每次儲存到服務端的時候,服務端會生成一個id,所以我們會判斷一下update數組裡面的每項的值,如果id屬性相等,則表示這個資料就是之前修改的資料,就不需要將此行資料加入到update陣列中,因為如果沒有這樣的判斷的話,當你每次點選儲存的時候,都會將當前行資料加入到update中,肯定不是我們想要的結果。最後取消按鈕就是將編輯狀態editIndex變成-1,還原成預設狀態。
最後來看看合計功能的實現,合計功能就是參照了element的合計來實現的
computed: {
summaryData () {
let sum = {}
let arr = []
this.summaryColumns.forEach((column, index) => {
if (index === 0) {
sum.total = '合計';
return;
}
if (column.key === 'handle' ) {
sum.handle = '';
return;
}
let values = this.tempSummary.map(item => Number(item[column.key]))
// 只要values中有一個值為NaN就不進行累加計算
if (!values.some(value => isNaN(value))) {
sum[column.key] = values.reduce((prev, curr) => {
const value = Number(curr);
if (!isNaN(value)) {
return prev + curr;
} else {
return prev;
}
}, 0);
sum[column.key] = sum[column.key].toFixed(2);
}
})
arr.push(sum)
return arr;
}
},
邏輯就是我們在表格的slot裡面又添加了一個table,這個table沒有表頭,只有資料項,我們根據主表格的陣列來進行累加,用的就是reduce歸併方法,不熟悉的同學可以去搜搜,然後規定第一列顯示合計的字樣,最後一列handle不進行操作,中間的資料項如果有一項不是數字的話,則不進行計算。當然這裡考慮到普遍適用性,沒有辦法給每一列的合計資料新增好一個單位,所以暫時是這樣,博主也是用了這樣一個思路,在element上面也封裝了一個近似的元件,因為這個iview的元件點選取消按鈕的時候回有延遲的情況,博主研究了很久,知識有限也是沒有找出原因,最後還是選擇用element來封裝,後續會將完整程式碼上傳到github上面,如果對您有幫助的話,希望能不吝star。後續github地址