背景
前端统计列表,数据乱序。按日期、产品、阶段、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">
<HandleList
v-model="pageData.versionCode"
:inputShow="false"
:search-show="false"
:export-show="true"
:add-show="false"
searchPlaceholder="版本号"
input-title="导出"
:exportDisabled="!tableData.length"
>
<el-date-picker
style="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-table
v-loading="loading"
id="stationTable"
class="table"
height="100%"
tooltip-effect="light"
:data="tableData"
:span-method="objectSpanMethods"
border
style="width: 100%"
>
<template v-for="cols in colConfigs">
<!-- 无需合并的列 -->
<el-table-column
v-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-column
v-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>
总结
我使用了一种笨方法实现了需求。主要是理解思路很重要。后面所有类型应该都能理解。
如果能解决问题,麻烦给我点个赞,非常感谢。
文章写得有点乱,新优化的代码写好了el-table多行合并-代码(二)-CSDN博客
请查收,记得点个赞