el-table多行合并
背景
前端统计列表,数据乱序。按日期、产品、阶段、DD项(所有header名称乱写)排序,列表如下。
示例
日期 | 产品 | 阶段 | DD | EE | FF | GG |
20240414 | 产品1 | 阶段1 | 场景1 | A | 0 | 1 |
场景2 | B | 0 | 1 | |||
其他 | A | 0 | 1 | |||
20240410 | 产品1 | 阶段1 | 场景2 | B | 0 | 1 |
其他 | A | 0 | 1 | |||
20240402 | 产品2 | 阶段1 | 场景3 | B | 0 | 1 |
场景4 | A | 0 | 1 | |||
场景5 | B | 0 | 1 | |||
场景6 | A | 0 | 1 | |||
产品1 | 阶段2 | 场景7 | B | 0 | 1 | |
场景9 | A | 0 | 1 | |||
场景8 | B | 0 | 1 | |||
阶段1 | 场景10 | A | 0 | 1 | ||
场景11 | B | 0 | 1 | |||
场景12 | A | 0 | 1 | |||
阶段1 | B | 0 | 1 | |||
场景2 | A | 0 | 1 | |||
其他 | B | 0 | 1 | |||
A | 0 | 1 | ||||
B | 0 | 1 | ||||
A | 0 | 1 | ||||
B | 0 | 1 | ||||
A | 0 | 1 | ||||
B | 0 | 1 | ||||
A | 0 | 1 | ||||
B | 0 | 1 | ||||
A | 0 | 1 | ||||
20240401 | 产品1 | 阶段2 | 场景7 | B | 0 | 1 |
阶段1 | 场景1 | A | 0 | 1 | ||
场景11 | B | 0 | 1 | |||
场景12 | A | 0 | 1 | |||
场景2 | B | 0 | 1 | |||
其他 | A | 0 | 1 | |||
B | 0 | 1 | ||||
A | 0 | 1 | ||||
B | 0 | 1 | ||||
A | 0 | 1 |
el-table项目合并方法
:span-method =“objectSpanMethods”
row行数据
column列数据
rowIndex 行下标
columnIndex 列下标
objectSpanMethods({ row, column, rowIndex, columnIndex }) {
// 列num_ 行num
let key = columnIndex + "_" + rowIndex;
if (this.tableMergeIndex[key]) {
return this.tableMergeIndex[key];
}
},
合并原理:
(0,0)2,1 | (1,0)1,1 | |
(1,1)1,1 | ||
(0,2)5,1 | (1,2)3,1 | |
(1,5)2,1 | ||
以行为例,不合并列,相当一个二维表格, (y,x) (0,2) 第一列 第三行 5,1 代表5行,一列,
其他
比如上表中 (0,1) 为 0,1 代表行的高度0,列为1个高度。
有个初步概念,如果我们要达到示例中的效果,我们应该怎么做。
- featch到的数据处理后然后按 属性'dt','p1','p2','p3' 排序 当然如果 有其他这种中文,自己考虑特殊处理,
- dt(日期好比较大小)
- p1 值固定 [1,2]
- p2 值固定 [1,2]
- p3 汉字排序,特殊 其他放在最后
- 把特殊的位置处理后,其他做填充, 我们特殊数组A tableMergeIndex= []
const key= columnIndex + "_" + rowIndex;
tableMergeIndex[key] = [ 行数,1]
比如 :tableMergeIndex['0_2'] = [5,1] ableMergeIndex['1_2'] =[3,1]
有了这些特殊的处理, 如果是前4列有合并 后面不需要合并
我们需要生成一个数组 B, 前四列 所有值 [0,1] 其他所有[1,1] 在把上面合并数组A 合并到
数组B就是我们的目标数组
-
for (let i = 0; i < this.tableData.length; i++) {for (let j = 0; j < this.mergeCols.length; j++) {const key = j + "_" + i;if (j < 4) {if (tableMergeIndex[key]) {newList[key] = tableMergeIndex[key];} else {newList[key] = [0, 1];}} else {newList[key] = [1, 1];}}}
(0,0)2-1 | (1,0)1-1 | |
(1,1)1-1 | ||
(0,2)5-1 | (1,2)3-1 | |
(1,5)2-1 | ||
思路
基于第2点考虑,怎么得到(0,2)[5,1]
(1,2) [3,1]其实从我们数据层面就能很好划分,从示例就很好看出来,日期 -》产品-》阶段-》DD
当前 20240414日中最大的行数 是3行, 产品、阶段、DD都不能超过3。所以有层级关系。
我的思路 :
(1)获取数据按属性升序或降序排序, 如dt-》 desc p1 -》asc
(2)按第1点中属性排序方式,获取每种属性出现的所有的值和具体次数,得到一个数组proptyNumList,用于合并项赋值用,数组格式如下
[
{ "20240401": 10,
"20240402": 22,
"20240410": 2,
"20240414": 2,
"20240415": 1
},
{
"1": 4, "2": 33
},
{ "1": 4, "2": 33
},
{1": 18, "2": 19
},
{ “场景7”:1, “场景2”:1,"其他": 19,
}
]
(3) 遍历每种属性 生成合并项的数据
这是第一项数据合并代码。我本来想写递归的,没想好。。。上线后优化吧。
let yNum0 = 0;for (let index0 = 0;index0 < propertyValuesNew[0].length;index0++) {let yNum1 = yNum0;const name0 = propertyValuesNew[0][index0];let key0 = "0_" + yNum0;yNum0 += propertyValues[0][name0];list[key0] = [propertyValues[0][name0], 1];}return list;
写在一个循环里面的。不敢展示代码,嵌套了4层。我写得真丑,
注意: let yNum0 = 0;
let yNum1 = yNum0;
let yNum2 = yNum1;
的位置 和list push的时候和内层循环的顺序。
propertyListDg(propertyValues, propertyValuesNew) {const list = [];let yNum0 = 0;for (let index0 = 0;index0 < propertyValuesNew[0].length;index0++) {let yNum1 = yNum0;const name0 = propertyValuesNew[0][index0];// const p0 = propertyValues[0][name0];for (let index1 = 0;index1 < propertyValuesNew[1].length;index1++) {let yNum2 = yNum1;const name1 = propertyValuesNew[1][index1];const num = this.getObjectsWithSameValues(this.tableData,["dt", "productType"],[name0, name1]).length;if (num > 0) {let key1 = "1_" + yNum1;yNum1 += num;list[key1] = [num, 1];}}// console.log(name0);// console.log(data1);let key0 = "0_" + yNum0;yNum0 += propertyValues[0][name0];list[key0] = [propertyValues[0][name0], 1];}return list;},
最后得到 合并数组 A, 再执行 el-table项目合并方法 中第二点 就可以了。
监听这个数据的变化 刷新table就行了 this.$refs.market.refresh();
方法汇总
其中几个用到的几个方法:
按属性排序
//属性排序数组 sortData: [["dt", "desc"],["p1", "asc"],["p2", "asc"],["p3", "asc"],["p4", "asc"],],
调用
this.sortByProperties(list, ...this.sortData);
sortByProperties(arr, ...props) {return arr.sort((a, b) => {for (let i = 0; i < props.length; i++) {const prop = props[i];const [key, order] = Array.isArray(prop)? prop: [prop, "asc"];// 获取属性值const valueA = a[key];const valueB = b[key];// 比较值,考虑升序和降序if (valueA < valueB) {return order === "asc" ? -1 : 1;}if (valueA > valueB) {return order === "asc" ? 1 : -1;}// 如果相等,比较下一个属性}// 所有属性都相等return 0;});},
获取属性出现的次数
getPropertyValues(array, propertys) {const list = [];for (let i = 0; i < propertys.length; i++) {const prop = propertys[i];const propertyValues = {};array.reduce((acc, item) => {const value = item[prop];if (value in acc) {acc[value]++;} else {acc[value] = 1;}return acc;}, propertyValues);list.push(propertyValues);}return list;},
调用
const propertyValues = this.getPropertyValues(this.tableData,this.sortData.map((item) => {return item[0];}));console.log(propertyValues);
获取对象列表属性具体值出现的次数
getObjectsWithSameValues(arr, properties, vals) {const list = arr.filter((item) => {let isRight = true;for (let index = 0; index < properties.length; index++) {const element = properties[index];if (item[element] != vals[index]) {isRight = false;}}return isRight;});// console.log("451", properties, vals, list.length);return list;},
调用
const name3 = propertyValuesNew4[index3]; const num = this.getObjectsWithSameValues(this.tableData,["dt","productType","stageType","scenarioNameNew",],[name0, name1, name2, name3]).length;
注意:里面的name0 name1 name2 name3 都是通过获取每个属性值出现的次数的方法拿到,再通过遍历每个属性的值数组 拿到。
另外种方式
思路 判断上一个和下一个是否相同。相同就+1然后递归。取到特殊值再合并数组。
但是会出现bug ,自己试试。
<template><div class="page"><HandleListv-model="pageData.versionCode":inputShow="false":search-show="false":export-show="true":add-show="false"searchPlaceholder="版本号"input-title="导出":exportDisabled="!tableData.length"><el-date-pickerstyle="width: 260px"class="ml10"v-model="chatDate"type="daterange"align="right"size="small"unlink-panels:start-placeholder="insertStr(startDay, '4|6', '-')":end-placeholder="insertStr(endDay, '4|6', '-')"value-format="yyyyMMdd"@change="changeDate":picker-options="pickerOptions"></el-date-picker></HandleList><div class="table"><el-tablev-loading="loading"id="stationTable"class="table"height="100%"tooltip-effect="light":data="tableData":span-method="objectSpanMethods"borderstyle="width: 100%"><template v-for="cols in colConfigs"><!-- 无需合并的列 --><el-table-columnv-if="cols.type === 'label' && !cols.children":key="cols.prop":prop="cols.prop":label="cols.label"></el-table-column><!-- 需要合并的列 --><template v-else-if="cols.type === 'label' && cols.children"><el-table-columnv-for="children in cols.children":key="children.prop":prop="children.prop":label="children.label"/></template></template></el-table></div></div>
</template><script>
import HandleList from "@/components/HandleList";
import { getDateForNum } from "@/utils/formatDate.js";
export default {name: "Table",components: { HandleList },data() {return {maxChatQueryDay: 7,chatDate: null,startDay: null,endDay: null,pageData: {startDay: 20231101,endDay: 20231120,versionCode: "",activityType: "首贷长尾,复贷有余额,复贷无余额,授信",productType: 1,},pickerOptions: {shortcuts: [{text: "最近一周",onClick(picker) {const end = new Date();const start = new Date();start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);picker.$emit("pick", [start, end]);},},{text: "最近一个月",onClick(picker) {const end = new Date();const start = new Date();start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);picker.$emit("pick", [start, end]);},},{text: "最近三个月",onClick(picker) {const end = new Date();const start = new Date();start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);picker.$emit("pick", [start, end]);},},],},loading: false,tableData: [{time: "2020-08-10",grade: "三年二班",name: "小明",subjects: "类型一",score: 80,score1: 1120,},{time: "2020-08-10",grade: "三年二班",name: "小明",subjects: "类型二",score: 80,score1: 1120,},{time: "2020-08-10",grade: "三年二班",name: "总成绩",subjects: "总成绩",score: 150,score1: 1120,},{time: "2020-08-10",grade: "三年二4班",name: "小明1",subjects: "类型一",score: 80,score1: 1120,},{time: "2020-08-10",grade: "三年二4班",name: "小明12",subjects: "类型二",score: 80,score1: 1120,},{time: "2020-08-10",grade: "三年二4班",name: "总成绩",subjects: "总成绩",score: 150,score1: 1120,},{time: "2020-08-10",grade: "三年一班",name: "小雷",subjects: "类型二",score: 70,score1: 1120,},{time: "2020-08-10",grade: "三年一班",name: "小雷",subjects: "类型一",score: 80,score1: 1120,},{time: "2020-08-10",grade: "三年一班",name: "总成绩",subjects: "总成绩",score: 150,score1: 1120,},{time: "2020-08-11",grade: "三年三班",name: "小花",subjects: "类型二",score: 60,score1: 1120,},{time: "2020-08-11",grade: "三年三班",name: "小花",subjects: "类型一",score: 601,score1: 1120,},{time: "2020-08-11",grade: "三年三班",name: "小花1",subjects: "类型一",score: 190,score1: 11201,},{time: "2020-08-11",grade: "三年三班",name: "小花1",subjects: "类型二",score: 190,score1: 11201,},{time: "2020-08-11",grade: "三年三班",name: "总成绩",subjects: "总成绩",score: 120,score1: 11201,},{time: "2020-09-11",grade: "三年三班",name: "小花1",subjects: "类型一",score: 190,score1: 1120,},{time: "2020-09-11",grade: "三年三班",name: "小花1",subjects: "类型二",score: 190,score1: 11201,},{time: "2020-09-11",grade: "三年三班",name: "总成绩",subjects: "总成绩",score: 120,score1: 11201,},],// 表格的信息 需要合并的需要放在 children 中colConfigs: [{type: "label",children: [{ prop: "time", label: "时间" },{ prop: "grade", label: "年级" },{ prop: "name", label: "姓名" },{ prop: "subjects", label: "科目" },{ prop: "score", label: "成绩" },{ prop: "score1", label: "成绩1" },],},// { type: 'label', prop: 'age', label: '年龄' }],// 需要合并的行列信息 index必须是table表格对应的下标 不能随意修改mergeCols: [{ index: 0, name: "time" },{ index: 1, name: "grade" },{ index: 2, name: "name" },{ index: 3, name: "subjects" },{ index: 4, name: "score" },{ index: 5, name: "score1" },// { index: 5, name: 'age' }],// 用来记录每一个单元格的下标tableMergeIndex: [],};},methods: {// 获取当前时间getCurrentTime() {const startDay = getDateForNum(-this.maxChatQueryDay).replace(/-/g, "");this.startDay = startDay;const endDay = getDateForNum(0).replace(/-/g, "");this.endDay = endDay;this.$set(this, "chatDate", [startDay, endDay]);this.pageData.startDay = startDay;this.pageData.endDay = endDay;},// 当前时间修改changeDate(val) {if (val) {this.pageData.startDay = val[0];this.pageData.endDay = val[1];} else {this.pageData.startDay = this.startDay;this.pageData.endDay = this.endDay;}this.fetch();},/**** @param {*} source 原数据* @param {*} start 下标位置用|隔开* @param {*} newStr 添加的符号*/// 日期格式修改:后台返回:20240101 =>insertStr(startDay, '4|6', '-')insertStr(source, start, newStr) {if (!source) {return "";}const list = start.split("|");list.forEach((num, index) => {const i = Number(num) + index;source = source.slice(0, i) + newStr + source.slice(i);});return source;},objectSpanMethods({ row, column, rowIndex, columnIndex }) {let key = columnIndex + "_" + rowIndex;if (this.tableMergeIndex[key]) {return this.tableMergeIndex[key];}},newTableMergeData() {for (let i = 0; i < this.tableData.length; i++) {for (let j = 0; j < this.mergeCols.length; j++) {// 初始化行、列坐标信息let rowIndex = 1;let columnIndex = 1;// 比较横坐标左方的第一个元素if (j > 0 &&this.tableData[i][this.mergeCols[j]["name"]] ===this.tableData[i][this.mergeCols[j - 1]["name"]]) {columnIndex = 0;}// 比较纵坐标上方的第一个元素if (i > 0 &&this.tableData[i][this.mergeCols[j]["name"]] ===this.tableData[i - 1][this.mergeCols[j]["name"]] &&["time", "grade", "name"].includes(this.mergeCols[j]["name"])) {rowIndex = 0;}// 比较横坐标右方元素if (columnIndex > 0) {columnIndex = this.onColIndex(this.tableData[i],j,j + 1,1,this.mergeCols.length);}// 比较纵坐标下方元素if (rowIndex > 0// &&["time", "grade", "name"].includes(this.mergeCols[j]["name"])// &&["time", "grade", "name"].includes(this.mergeCols[j]["name"])) {// console.log(i, i + 1, 1, this.mergeCols[j]["name"]);rowIndex = this.onRowIndex(this.tableData,i,i + 1,1,this.mergeCols[j]["name"]);}let key = this.mergeCols[j]["index"] + "_" + i;this.tableMergeIndex[key] = [rowIndex, columnIndex];}}},/*** 计算列坐标信息* data 单元格所在行数据* index 当前下标* nextIndex 下一个元素坐标* count 相同内容的数量* maxLength 当前行的列总数*/onColIndex(data, index, nextIndex, count, maxLength) {// 比较当前单元格中的数据与同一行之后的单元格是否相同if (nextIndex < maxLength &&data[this.mergeCols[index]["name"]] ===data[this.mergeCols[nextIndex]["name"]]) {return this.onColIndex(data, index, ++nextIndex, ++count, maxLength);}return count;},/*** 计算行坐标信息* data 表格总数据* index 当前下标* nextIndex 下一个元素坐标* count 相同内容的数量* name 数据的key*/onRowIndex(data, index, nextIndex, count, name) {const nameIndex = this.mergeCols.findIndex((i) => {return i.name === name;});const tIndx = nameIndex || 1;const tName = this.mergeCols[tIndx].name;// 比较当前单元格中的数据与同一列之后的单元格是否相同if (nextIndex < data.length &&data[index][name] === data[nextIndex][name] &&// data[index][tName] === data[nextIndex][tName] &&["time", "grade", "name"].includes(name)) {return this.onRowIndex(data, index, ++nextIndex, ++count, name);}return count;},},mounted() {if (this.mergeCols.length > 0) {this.newTableMergeData();}this.getCurrentTime()},
};
</script><style lang="scss" scoped></style>
总结
我使用了一种笨方法实现了需求。主要是理解思路很重要。后面所有类型应该都能理解。
如果能解决问题,麻烦给我点个赞,非常感谢。