1. 程式人生 > >iview可編輯表格元件封裝

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地址