你还在为手写el-table表格合并方法而苦恼吗

3,364 阅读10分钟

我正在参加「掘金·启航计划」

前言

  大家好,我是前端贰货道士。最近,在处理收单系统的过程中,发现在很多模块,都有使用到表格合并的功能。于是我决定抽出时间,整理下element的表格合并思路。另外,最近苦于自己蹩脚的口才,不懂得如何从生活中去培养和提升自己的幽默细胞,如何成为一个富有高情商的人才。查询未果,烦请大家在评论区给出指点江山的传送门,不胜感激。 最后, 如果本文对您有那么一丝一毫的启发,烦请大家一键三连哦, 蟹蟹大家~

浅析element表格合并方法

  在处理这个问题之前,我们必须得先对element的表格合并方法有一个大致的理解和认识:element官方的表格合并方法。该方法有一个参数,解构出来有四个常用的表格参数,分别是row(表格的每行数据), column(element对列的封装,是一个包含很多信息的对象,其中我们需要重点关注property这个属性,这个属性就是el-table组件对应的el-table-column所绑定的prop), rowIndex(表格的行索引) 以及columnIndex(表格的列索引)。

  我们可以根据某一列 (使用columnIndex进行判断,例:columnIndex === 0),即针对第一列的每一个小的单元格, 合并需要合并的行数和列数。而在合并方法中,返回合并数目的形式无非如下两种:

  • return [1, 2] // 长度为2的数组,第一个数为需要合并的行数,第二个数为需要合并的列数,更推荐这种简便的写法
  • return {rowspan: 1, colspan: 2} // 有两个属性的对象,rowspan表示需要合并的行数,colspan表示需要合并的列数

Tips: 在自定义列模板时,虽然官方文档上没有写,但其实我们也可以在el-table-column上使用prop名称,来指定这一列的property

image.png

合并效果镇楼

  话不多说,先上表格合并的效果图:

image.png

image.png

表格合并规则思路分析(以商品数据为栗)

1. 前端需要请求接口,获取并格式化后端返回的商品数据

  后端给定的数据,往往不是我们最终想要得到的数据。假定后端返回的数据结构是这个样子:

data() {
    return {
      data: [
        {
          id: 1,
          chineseName: 'cxk同款卫衣',
          styleList: [{ styleName: '唱' }, { styleName: '跳' }, { styleName: 'rap' }],
          sizeList: [{ sizeName: 'M' }]
        },
        {
          id: 2,
          chineseName: 'gqq同款兵法',
          styleList: [{ styleName: 'rap' }, { styleName: '孙子兵法' }],
          sizeList: [{ sizeName: 'S' }, { sizeName: 'L' }]
        }
      ]
    }
}

  因为后端逻辑是款式决定尺码,每个款式下都有对应的尺码信息。所以,我们需要对数据进行重构,最终重构的商品数据,是款式和尺码的笛卡尔积。

`格式化后端返回的商品数据:`
computed: {
    finalData() {
      const data = []
      this.data.map((item) => {
        const { id, chineseName } = item
        item.styleList.map(({ styleName }) => {
          item.sizeList.map(({ sizeName }) => {
            data.push({
              id,
              chineseName,
              styleName,
              sizeName
            })
          })
        })
      })
      return data
    }
}

  给定finalData数据,最终通过el-table渲染出来的表格样式如下:

image.png

2. 必须指定表格关联规则(重点,后续不再赘述)

  何为指定表格的关联规则?怎样的关联规则才是一个比较好的规则呢?通过图片不难发现,合并是需要我们把相同的单元项合并到一起。但这往往不能满足我们的真实需求。举栗来说,如果第三行和第四行的款式数据合并到一起,这就是一个错误的合并。因为我们忽略了一个大前提,那就是相同的单元项必须得是同一件商品。 所以,对于这个表格来说,id相同的id列数据,需要合并在一起。商品名称和商品款式的单元格合并,需要建立在id相同的前提下,而对于尺码列来说, 尺码的单元格合并,需要建立在id相同且款式相同的前提下。 这就是一个大规则,所以我们希望建立一个映射关系:

`
构建一个二维数组映射关系:

1. 对于最内层数组中的每一项,左边的代表绑定表格的字段名称,右边的代表el-table-column
所绑定的prop值(及每一列的property)所构成的数组,数组中每一项的合并规则都和左边绑定表格的字段名称挂钩;

2. 越往后靠的对象的key值,必须建立在同时满足前面所有key值和本身key值都相同才合并的规则上;

3. 我们还可以对最内层数组进行遍历,给定多个规则且用数组包裹,让不同数组中的字段互不关联;

4. 我们还可以对对象中的每一项添加同级关系的映射关系
`

props: [
  [
    `相互关联的情况:下层的key值依赖上层所有key值都相同才合并`
    {
      id: ['id', 'chineseName']
    },
    {
      styleName: ['styleName']
    },
    {
      sizeName: ['sizeName']
    }
  ]
]

props: [
  [
    `这个数组中的key值相互关联`
    {
      id: ['id', 'chineseName']
    },
    
    {
      styleName: ['styleName']
    }
  ],
  
  [
    `这个数组中的key值相互关联,与上一个数组中的key值相互独立`
    {
      sizeName: ['sizeName']
    }
  ]
]

props: [
  [
    `这个数组中的key值相互关联`
    {
      id: ['id', 'chineseName']
    },
    
    {
      `styleName和sizeName为同级关系,都依赖前置key值id`
      styleName: ['styleName'],
      sizeName: ['sizeName']
    }
  ]
]
  • 使用第一个规则的props合并的效果(id、款式、尺码都相同时,才合并尺码单元格):

image.png

  • 使用第二个规则的props合并的效果(尺码相同则合并):

image.png

3. 需要考虑一种比较极端的情况(一般不会出现)

  有一种比较极端的情况是,后端返回的数据本身存在错误。比如后端返回了具有相同id的数据:

data() {
    return {
      data: [
        {
          id: 1,
          chineseName: 'cxk同款卫衣',
          styleList: [{ styleName: '唱' }, { styleName: '跳' }, { styleName: 'rap' }],
          sizeList: [{ sizeName: 'M' }]
        },
        {
          id: 2,
          chineseName: 'gqq同款兵法',
          styleList: [{ styleName: '孙子兵法' }],
          sizeList: [{ sizeName: 'S' }, { sizeName: 'L' }]
        },
        {
          id: 1,
          chineseName: 'cxk同款卫衣',
          styleList: [{ styleName: 'rap' }, { styleName: '跳' }, { styleName: '唱' }],
          sizeList: [{ sizeName: 'S' }]
        }
      ]
    }
}

  那么最终通过表格组件渲染出来的数据,会是这样一个效果。很显然,图中用红线圈起来的两条数据是无法合并的,因为它们之间隔了两条数据。所以虽然它们具有相同的id,商品名称和款式,但它们是无法通过表格合并规则合并到一起的。

image.png

封装表格合并公共方法的分步解析

`封装全局$GET方法:`
`lodash中的get方法存在一个缺陷:如果取的的字段值为null,就会取null, 而不会拿给定的默认值`

import { get } from 'lodash'

`在vue的原型对象上挂载$GET方法,这样在取值为null时,就会选择给定的默认值:`
Vue.prototype.$GET = window.$GET = (object, path, defaultValue) => {
  return get(object, path, defaultValue) || defaultValue
}
import { isPlainObject, isNumber, flatten } from 'lodash'

`主方法:`

`合并方法共包含三个参数:第一个参数就是element合并方法自带的参数,第二个参数就是绑定的表格数据,
第三个参数则是一些配置项。而在第三个参数解构出来的变量中,

props: 前文已提及过,用于配置表格合并的映射关系
emptyProp:一般用于表格操作列的字段名称
closSpan:对象,用于配置el-table-column所绑定的prop值对应单元格合并的列数。eg: { chineseName: 5 }
`

export function createSpanMethod({ row, column, rowIndex, columnIndex }, data = [], option) {
  
  const { props = [], emptyProp = 'done', closSpan = {} } = option
  
  `获取每一列的prop值`
  let { property = 'done' } = column
  if (!property) property = emptyProp
  
  `获取要合并的propert映射的筛选条件`
  const filterProps = getMappingFilterProp(property, props)
  
  `未匹配,则不需要合并单元格`
  if (!filterProps.length) return [1, 1]
  
  `将符合筛选条件的连续数据分组`
  const splitData = getSplitData(data, row, filterProps)
  
  `获取到要合并的条数`
  const mergeCount = getMergeCount(splitData, rowIndex)
  let cloCount = 1
  
  `重置column方向合并的条数`
  if (isPlainObject(closSpan) && isNumber(closSpan[property])) cloCount = closSpan[property]
  return [mergeCount, cloCount]
}
`获取筛选条件:第一个参数为该列的property字段,第二个参数为我们传入的props映射关系`
`返回结果: 为一个包含表格字段的数组,即最内层映射关系的key值所组成的数组`

export function getMappingFilterProp(property, props) {
  let filterProps = []
  ;(() => {
    const len = props.length
    `因为是二维数组,所以需要进行双重遍历`
    for (let i = 0; i < len; i++) {
      let arr = props[i]
      const len1 = arr.length
      for (let j = 0; j < len1; j++) {
        `找到当前的映射对象`
        let item = arr[j]
        
        `判断当前映射对象包含的数组是否存在property字段`
        `此处用flatten是为了对传入的数组进行扁平化`
        const isFind = flatten(Object.values(item)).includes(property)
        `如果不存在,则将映射对象的key值打入filterProps数组中,继续遍历二维数组往下找`
        if (!isFind) filterProps.push(...Object.keys(item))
        `如果存在,则将映射对象的key值打入filterProps数组中,并跳出立即执行函数,直接返回filterProps`
        `此处用遍历的写法是为了兼容一个映射对象有多个key值的情况, 并只push那一个key值`
        `如果此时push所有key值,字段就成 && 而不是 我们想要的 || 关系了`
        `比如:a是第一级,b和c是第二级,如果push所有key值,则返回['a', 'b', 'c']`
        `而我们想要的效果仅仅是['a','b']和['a','c']`
        else for (let key in item) {
          if (item[key].includes(property)) return filterProps.push(key)
        }
      }
      `在每次最外层循环之后,置空filterProps数组,开启第二轮循环,达到相互独立的效果`
      filterProps = []
    }
  })()
  return filterProps
}
`获取筛选条件的方法:第一个参数为表格数据,第二个参数为当前表格行数据,第三个参数为我们得到的关联key数组`
`返回结果为需要合并的行索引位置的二维数组`
export function getSplitData(data, row, filterProps) {
  let preIndex = -1
  let splitData = []
  `对表格数据进行遍历,并和当前数据行的关联key数组的每一个值进行比较。如果都相等,则说明这是同一条需要合并的数据`
  `如果不相等,则表格的这行数据不是我们需要继续合并的位置,则直接return掉,并开始记录下段需要合并的位置数组`
  data.map((item, index) => {
    const isSame = filterProps.every((prop) => {
      const originValue = $GET(item, prop, null)
      const currentValue = $GET(row, prop, null)
      if (!originValue || !currentValue) return false
      return originValue == currentValue
    })
    if (!isSame) return
    
    `preIndex表示上次需要合并的行索引的位置`
    `index-1表示当前行索引-1的位置`
    `preIndex != index - 1这一步很关键,是处理下图的特殊情况`
    `这种特殊情况是:假定有3条id为1的数据,有4条id为2的数据,最后有3条id为1的数据`
    `preIndex != index - 1的作用是判断上次需要合并的行索引位置和当前行索引-1的位置是否相等`
    `如果相等,则表示是连续的,需要合并的行`
    `如果不相等,则表示是间断的,则新开一个数组记录这次新的需要合并的行索引的位置`
    `对应这个特殊情况,最后的执行结果为[[0, 1, 2], [7, 8, 9]]`
    `开始都是相同的,且preIndex == -1,preIndex == index - 1, 所以执行结果为[[0, 1, 2]]`
    `到索引为3, 4, 5, 6的时候,发现不是同一条数据了,则直接return掉了`
    `继续遍历表格,到索引为7的时候,发现 7 != 2 - 1,则push新数组,记录新的合并关系`
    `最后执行出来得到[[0, 1, 2], [7, 8, 9]]`
    if (preIndex == -1 || preIndex != index - 1) {
      splitData.push([])
    }
    preIndex = index
    `splitData.slice(-1)[0]表示取到最后一个二维数组中的那个一维数组`
    `并向里面push需要合并的行索引的位置`
    splitData.slice(-1)[0].push(index)
  })
  return splitData
}

image.png

`获取需要合并的行数,第一个参数为二维数组,第二个参数为合并的行索引`
export function getMergeCount(splitData, rowIndex) {
  `对二维数组进行遍历,找到需要合并的行索引的位置`
  const findData = splitData.find((indexData) => indexData.includes(rowIndex))
  `如果找不到,就不需要合并`
  if (!findData) return 1
  `如果找得到,且第一个需要合并的行索引和当前行索引相等,说明这是第一个需要合并的行索引的位置,
  则合并数组长度个行数据`
  if (findData[0] == rowIndex) return findData.length
  `否则,则是后续需要合并的位置,直接返回0即可,因为上面已经做了合并操作`
  return 0
}

公共代码整合及应用实例

import { isPlainObject, isNumber, flatten } from 'lodash'

//[{a: [b]}, {c: [d], e: [f]}] [[{a: []}], []]
/**
 * @description: 创建row 合并
 * @param {*} row table row
 * @param {*} column table  column
 * @param {*} rowIndex table rowIndex
 * @param {Array} data 列表页渲染的数据
 * @param {Object} option {
 *  props: [
    [
      { id: ['combinedInfo', 'createByName', 'createTime', 'done'] },
      { combinedColorName: ['combinedColorName'] }]
    ]
  ]
    二维数组 描述row property 中之间的映射关系

  closSpan:{
    combinedInfo: 5 //重置对应键值合并的clo
  }

 * }
 * @return {Array} span 合并的数组
 */

//示例
/** createSpanMethod(
 { row, column, rowIndex },
 this.finalData,
 {
  props: [
    [{ id: ['combinedInfo', 'createByName', 'createTime', 'done'] }, { combinedColorName: ['combinedColorName'] }]
  ],
  closSpan:{
    combinedInfo: 5
  }
})
 **/
 
export function createSpanMethod({ row, column, rowIndex, columnIndex }, data = [], option) {
  const { props = [], emptyProp = 'done', closSpan = {} } = option
  let { property = 'done' } = column
  if (!property) property = emptyProp
  
  //获取要合并的propert映射的筛选条件
  const filterProps = getMappingFilterProp(property, props)
  //未匹配,表示未合并的单元格
  if (!filterProps.length) return [1, 1]
  //将符合筛选条件的连续数据分组
  const splitData = getSplitData(data, row, filterProps)
  //获取到要合并的条数
  const mergeCount = getMergeCount(splitData, rowIndex)
  let cloCount = 1
  //重置column方向合并的条数
  if (isPlainObject(closSpan) && isNumber(closSpan[property])) cloCount = closSpan[property]
  return [mergeCount, cloCount]
}

/**
 * @description: 获取筛选条件
 * @param {String} property
 * @param {Array} props 二维数组 筛选所有属性的交集
 * @return {Array}
 */

export function getMappingFilterProp(property, props) {
  let filterProps = []
  ;(() => {
    const len = props.length
    for (let i = 0; i < len; i++) {
      let arr = props[i]
      const len1 = arr.length
      for (let j = 0; j < len1; j++) {
        let item = arr[j]
        const isFind = flatten(Object.values(item)).includes(property)
        if (!isFind) filterProps.push(...Object.keys(item))
        else return filterProps.push(...Object.keys(item))
      }
      filterProps = []
    }
  })()
  return filterProps
}

/**
 * @description: 按照索引值拆分数组
 * @param {Array} data 列表数据
 * @param {Object} row
 * @param {Array} filterProps 筛选列表
 * @return {Array}
 */
export function getSplitData(data, row, filterProps) {
  let preIndex = -1
  let splitData = []
  data.map((item, index) => {
    const isSame = filterProps.every((prop) => {
      const originValue = $GET(item, prop, null)
      const currentValue = $GET(row, prop, null)
      if (!originValue || !currentValue) return false
      return originValue == currentValue
    })
    if (!isSame) return
    if (preIndex == -1 || preIndex != index - 1) {
      splitData.push([])
    }
    preIndex = index
    splitData.slice(-1)[0].push(index)
  })
  return splitData
}

/**
 * @description: 获取row合并的数值
 * @param {Array} splitData
 * @param {Number} rowIndex
 * @return {Number}
 */
export function getMergeCount(splitData, rowIndex) {
  const findData = splitData.find((indexData) => indexData.includes(rowIndex))
  if (!findData) return 1
  if (findData[0] == rowIndex) return findData.length
  return 0
}
<template>
  <el-table :data="finalData" border style="width: 700px" :span-method="spanMethod">
    <el-table-column prop="id" label="id"> </el-table-column>
    <el-table-column prop="chineseName" label="商品"> </el-table-column>
    <el-table-column prop="styleName" label="款式"> </el-table-column>
    <el-table-column prop="sizeName" label="尺码"> </el-table-column>
  </el-table>
</template>

<script>
import { createSpanMethod } from '@/utils'

export default {
  computed: {
    finalData() {
      const data = []
      this.data.map((item) => {
        const { id, chineseName } = item
        item.styleList.map(({ styleName }) => {
          item.sizeList.map(({ sizeName }) => {
            data.push({
              id,
              chineseName,
              styleName,
              sizeName
            })
          })
        })
      })
      return data
    }
  },

  data() {
    return {
      data: [
        {
          id: 1,
          chineseName: 'cxk同款卫衣',
          styleList: [{ styleName: '唱' }, { styleName: '跳' }, { styleName: 'rap' }],
          sizeList: [{ sizeName: 'M' }]
        },
        {
          id: 2,
          chineseName: 'gqq同款兵法',
          styleList: [{ styleName: 'rap' }, { styleName: '孙子兵法' }],
          sizeList: [{ sizeName: 'S' }, { sizeName: 'L' }]
        }
      ]
    }
  },

  methods: {
    `表格合并方法的运用:`
    spanMethod(params) {
      return createSpanMethod(params, this.finalData || [], {
        props: [
          [
            {
              id: ['id', 'chineseName']
            },
            {
              styleName: ['styleName']
            },
            {
              sizeName: ['sizeName']
            }
          ]
        ]
      })
    }
  }
}
</script>

  最终效果:

image.png

结语

  jym,你们学废了吗?大概就这样吧~