封装Vue竖直纵向表头左右结构的table表格

我们前端开发人员在使用表格的过程中,大概率碰到的都是表格头部在表格的最上边,然后呈一行展示,紧接着就是表格的每一行的每一个单元格来展示具体内容的场景,很少会遇到表格的头部呈纵向一行展示,也就是说表格的头部在左边,具体的内容在右边(也可以说是左右结构)这种使用场景,而且有时候的场景可能会是纵向表头有两列或多列。

遇到这样的需求或者说使用场景,你也不能说人家产品提的需求不合理,毕竟使用场景不同。我不知道咱们同行的前端大佬是用啥牛逼格拉斯的技术来实现这样的需求的,反正我以前基本都是直接上一个表格,然后各种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]
    if(lastTwo){
      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]
if(lastTwo){
   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设置成几就可以了,非常方便。

posted @ 2021-04-01 14:30  豫见陈公子  阅读(6776)  评论(4编辑  收藏  举报