封裝Vue縱向表頭左右結構的table表格

豫見陳公子發表於2021-04-01

我們前端開發人員在使用表格的過程中,大概率碰到的都是表格頭部在表格的最上邊,然後呈一行展示,緊接著就是表格的每一行的每一個單元格來展示具體內容的場景,很少會遇到表格的頭部呈縱向一行展示,也就是說表格的頭部在左邊,具體的內容在右邊(也可以說是左右結構)這種使用場景,而且有時候的場景可能會是縱向表頭有兩列或多列。

遇到這樣的需求或者說使用場景,你也不能說人家產品提的需求不合理,畢竟使用場景不同。我不知道我們們同行的前端大佬是用啥牛逼格拉斯的技術來實現這樣的需求的,反正我以前基本都是直接上一個表格,然後各種tr、td的往上堆,如果需要展示的多了,最後會發現整個頁面上基本全是各種tr、td的標籤,Level Low就不說了,關鍵是看著鬧心、煩心外加噁心。

最近想著說再遇到這樣的需求,可不能再各種tr、td往上堆了,恰好近期的需求開發上也有這樣的使用場景,索性就動動腦筋自己封裝一個吧,以後用起來也方便,也能提高開發效率,介面看起來也能清爽不少。

一開始搞的時候,確實沒頭緒,很懵圈。上網搜了一把,想著說看看能不能找點靈感,或者說有好的例子能借鑑一下。結果一圈下來,發現就算是有人搞過這樣的封裝,也是感覺詞不達意,說好的封裝呢?有人基於elementUI就實現了一列縱向表頭的封裝,有人基於索引實現了兩列縱向表頭的封裝,但程式碼中出現的各種數字令人費解,不利於擴充套件。

算了,還是自己想辦法吧。不有句話說的好嘛:“人的腦洞有多大,就能實現多牛逼的需求。”於是,對著設計稿上這樣的表格,我是左看右看,上看下看,後來突然想到我們經常使用的表單元件,大概率不就是左右結構嘛,基於這個我又想到曾經在封裝Vue Element的form表單元件的時候還使用過分段的思想,現在把這個分段的思想用在這裡不正好嗎?

辦法總比困難多,這句老俗語總結的真是太TMD的完全正確了!

照例先上一張效果圖:

1、封裝的縱向表頭表格元件Table.vue

<template>
  <table class="portait-table" border="1" cellspacing="0" cellpadding="0">
    <tr v-for="(row, i) in columns" :key="i">
      <template v-for="x in row">
        <TD :config="x" :data="data" />
      </template>
    </tr>
  </table>
</template>

<script>
import * as Components from '@/components/table/cell/components'
import { chunk } from '@/utils'
import { noop } from '@/common/constant'

export default {
  props: {config: Object},
  data() {
    const {headers, data, rowSize = 2} = this.config || {}
    return {
      headers,
      data,
      rowSize,
    };
  },
  computed: {
    columns: ({headers, rowSize}) => chunk(headers, rowSize)
  },
  components: {
    TD: {
      functional: true,
      props: {config: Object, data: Object},
      render: (h, {props: {config, data}}) => {
        let {type = 'Default', label, prop} = config, value = data[prop], isEmpty = value === '' || value === undefined || value === null, children = noop
        
        if(label && isEmpty) children = h(Components.Default, {props: {value: '-'}})
        else children = h(Components[type], {props: {value, data, ...config}})

        return [h('td', label), h('td', [children])]
      }
    }
  },
  mounted(){
    const {columns} = this, last = columns[columns.length - 1], lastTwo = columns[columns.length - 2]
    for(let i = 0; i < (lastTwo.length - last.length); i++){
      this.headers.push({prop: '', label: ''})
    }
  },
}
</script>

<style lang="scss" scoped>
.portait-table{
  border-collapse: collapse;
  border: none;
  width:100%;
  td {
    border: 1px solid #F3F3F3;
    height: 40px;
    padding-left: 10px;
    color:#333;
    &:nth-child(odd){
      background: #EAEAEA;
      color: #454545;
    }
  }
}
</style>

對於以上元件中的程式碼,需要做一些以下說明。

2、分段工具的實現chunk

export const chunk = (arr, size) => {
    if(!arr.length || size < 1) return [];
    let list = [], index = 0, resIndex = 0, len = arr.length;
    while (index < len) {
        list[resIndex++] = arr.slice(index, index += size);
    }
    return list;
}

陣列被分段後的效果,你可以自己列印出來看看就明白了。

3、封裝的元件中引入了一些其他的元件是幹嘛使的呢?比如:
import * as Components from '@/components/table/cell/components'
引入這些元件,主要是為了對一些特殊的欄位值進行特殊的處理,比如金額千分位、時間戳格式化、列舉(對映)等,這些元件的具體程式碼和介紹在我的這篇博文封裝Vue Element的table表格元件中有具體的描述,您可以擺駕過去上上眼。

4、程式碼中的render函式,這裡也不再贅述,我之前的博文中都有介紹,你也可以自己去看vue的API或查閱其他資料。

5、在封裝的元件的mounted生命週期裡有這樣一段程式碼:

const {columns} = this, last = columns[columns.length - 1], lastTwo = columns[columns.length - 2]
for(let i = 0; i < (lastTwo.length - last.length); i++){
   this.headers.push({prop: '', label: ''})
}

這段程式碼是幹嘛滴的呢?我們都知道,像這樣的縱向表頭左右結構展示的表格,不管你分了幾列表頭(只有一列表頭的除外)展示,都有可能在表格的最後一行出現不完整的情況,也就是說可能會在整個表格的右下角出現空缺的情況,如下圖:

這種類似表格殘缺不全的情況當然是不被允許的,mounted生命週期中的那段程式碼就是用來補這個缺的。其原理就是拿分段後的最後一個陣列長度與倒數第二個陣列長度進行比較,如果兩者不相等,則最後一個陣列長度比倒數第二個陣列長度少了幾個,就在headers陣列的最後push幾個屬性值是空的的物件,最後再利用計算屬性去重新分段(元件中的計算屬性會被執行兩次,第一次是初始分段,第二次是補缺後的再次分段),就達到了本文開頭展示的補缺後的效果圖。

其實,程式碼中真正補缺的原理可能與我描述的實現不太一樣,比如程式碼中並沒有拿分段後的最後一個陣列長度與倒數第二個陣列長度進行比較,而是用了一個for迴圈就搞定了,因為for迴圈也有自己的判斷嘛,但是原理真的差不離。

6、至於那個noop,它其實就是定義了一個能返回空物件的函式export const noop = () => {},它在封裝中的作用就是用來初始化函式。還有那個rowSize,是用來設定分成幾段的,預設是2段。

7、封裝時用到了computed計算屬性,這個屬性想必大家已經很熟了,這裡再多說一句吧:計算屬性的key的值是一個函式,其引數是Vue的例項化this物件。為啥這裡要強調這一點呢,是因為知道了這一點,我們就可以在其函式的引數中直接解構this了,比如:

computed: {
  columns: ({headers, rowSize}) => chunk(headers, rowSize)
},

而我們平時常用的寫法是:

computed: {
  columns(){
    const {headers, rowSize} = this
    return chunk(headers, rowSize)
  }
},

兩廂對比,哪種寫法更優雅、效能更高呢?當然是前者了。

8、使用封裝後的表格Table元件

<template>
  <Table :config="config" />
</template>

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

export default {
  components: {
    Table
  },
  data(){
    return {
      config: {
        headers: [
            {prop: 'ruleName', label: '規則名稱'},
            {prop: 'money', label: '執行金額', type: 'Currency'},
            {prop: 'fileName', label: '附件名稱'},
            {prop: 'ruleRiskLevel', label: '預警顏色'},
            {prop: 'monitorResult', label: '監控結果', type: 'Enum', Enum: {name: 'monitor'}},
            {prop: 'productCode', label: '產品系列'},
            {prop: 'date', label: '執行時間', type: 'Date', format: 'yyyy-MM-dd'},
        ],
        rowSize: 2,
        data: {
          ruleName: '股權質押',
          money: 3256898,
          fileName: '',
          ruleRiskLevel: '紅色',
          monitorResult: '00',
          productCode: '事業部',
          date: new Date().getTime(),
        }
      }
    }
  },
}
</script>

本文到此,基本就實現了縱向表頭的table表格封裝,想展示幾列表頭,就把rowSize設定成幾就可以了,非常方便。

相關文章