我正在参加「掘金·启航计划」
前言
大家好,我是前端贰货道士。最近,在处理收单系统的过程中,发现在很多模块,都有使用到表格合并的功能。于是我决定抽出时间,整理下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。
合并效果镇楼
话不多说,先上表格合并的效果图:
表格合并规则思路分析(以商品数据为栗)
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渲染出来的表格样式如下:
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、款式、尺码都相同时,才合并尺码单元格):
- 使用第二个规则的
props合并的效果(尺码相同则合并):
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,商品名称和款式,但它们是无法通过表格合并规则合并到一起的。
封装表格合并公共方法的分步解析
`封装全局$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
}
`获取需要合并的行数,第一个参数为二维数组,第二个参数为合并的行索引`
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>
最终效果:
结语
jym,你们学废了吗?大概就这样吧~