前言
做了一个业务线上的客户定制化报表需求,因为报表的各种合并行,合并列,静态动态按需拼接数据等一系列操作,日常使用的 帆软报表平台
,公司新推的 自定义报表平台
在实现方面可能存在问题,由于年底事情多,人手也不够,各种任务时间排期还特别紧,经组内前端讨论,我们决定用比较稳妥靠谱的方式,手搓了几个定制化报表,由于之前没有做过这么多的自定义合并行列的表格,这次开发实现过程也是一个探索的过程
最终效果
先看下定制化报表的最终效果
这里简单介绍一下里面的业务逻辑
查询逻辑
基于日期、片区、猪场条件查询数据,日期必选,片区和猪场如果没有选择,默认查询所有猪场的数据,片区和猪场是联动效果,如果选了片区,则过滤当前片区下的所有猪场
数据展示
如上图所示,先看报表标题头部,一共三行,前面片区静态,后面合计静态,中间的片区,猪场,小计数据为动态,猪场下面对应的三列数据是静态
接下看表格数据区域,阶段列中的数据是根据查询结果动态显示,同类型需要合并行,平均存栏和死淘率这两个行数据需要合并列,其他数据默认不合并
思路分析
当前定制化报表的需求清楚了,来分析一下实现思路,首先这个报表不是普通的表格,不能直接进行数据绑定,动态标题头和内容需要数据单独拼接,同时需要数据合理处理报表中行列的动态静态合并
实现过程
返回接口数据格式
json
{
"code": 0,
"data": {
"headerArea": [
{
"Key": "2311081103550000150",
"Name": "片区01",
"Value": "2311081103550000150",
"Row": 1,
"Column": 9
},
...
{
"Key": "Total",
"Name": "合计",
"Value": "",
"Row": 1,
"Column": 2
}
],
"headerPigFarm": [
{
"Key": "2311081103550000150.2203141401040000076",
"Name": "母猪场活跃度测试",
"Value": "2203141401040000076",
"Row": 1,
"Column": 3
},
{
"Key": "2311081103550000150.2203141401380000076",
"Name": "肥猪场活跃度测试",
"Value": "2203141401380000076",
"Row": 1,
"Column": 3
},
{
"Key": "2311081103550000150.Subtotal",
"Name": "小计",
"Value": "",
"Row": 1,
"Column": 3
}
...
],
"headerColumn": [
{
"Key": "2311011538400000150.2105191446220001076.Death",
"Name": "死亡",
"Value": "",
"Row": 1,
"Column": 1
},
{
"Key": "2311011538400000150.2105191446220001076.Eliminate",
"Name": "无价淘",
"Value": "",
"Row": 1,
"Column": 1
},
{
"Key": "2311011538400000150.2105191446220001076.Valuable",
"Name": "有价淘",
"Value": "",
"Row": 1,
"Column": 1
},
{
"Key": "2311011538400000150.Subtotal.Death",
"Name": "死亡",
"Value": "",
"Row": 1,
"Column": 1
},
{
"Key": "2311011538400000150.Subtotal.Eliminate",
"Name": "无价淘",
"Value": "",
"Row": 1,
"Column": 1
},
...
],
"bodyData": [
{
"PigType": "201911041541201001",
"PigTypeName": "仔猪",
"DataDete": "2023-11-01T00:00:00",
"2311011538400000150.2105191446220001076.Death": 0,
"2311011538400000150.2105191446220001076.Eliminate": 0,
"2311011538400000150.2105191446220001076.Valuable": 0,
"2311011538400000150.Subtotal.Death": 0,
"2311011538400000150.Subtotal.Eliminate": 0,
"2311011538400000150.Subtotal.Valuable": 0,
"2311081103550000150.2203141401040000076.Death": 0,
"2311081103550000150.2203141401040000076.Eliminate": 0,
"2311081103550000150.2203141401040000076.Valuable": 0,
"2311081103550000150.2203141401380000076.Death": 0,
"2311081103550000150.2203141401380000076.Eliminate": 0,
"2311081103550000150.2203141401380000076.Valuable": 0,
"2311081103550000150.Subtotal.Death": 0,
"2311081103550000150.Subtotal.Eliminate": 0,
"2311081103550000150.Subtotal.Valuable": 0,
"Total.Death": 0,
"Total.Eliminate": 0,
"Total.Valuable": 0
},
{
"PigType": "201911041541201001",
"PigTypeName": "仔猪",
"DataDete": "2023-11-02T00:00:00",
"2311011538400000150.2105191446220001076.Death": 2,
"2311011538400000150.2105191446220001076.Eliminate": 2,
"2311011538400000150.2105191446220001076.Valuable": 1,
"2311011538400000150.Subtotal.Death": 2,
"2311011538400000150.Subtotal.Eliminate": 2,
"2311011538400000150.Subtotal.Valuable": 1,
"2311081103550000150.2203141401040000076.Death": 9,
"2311081103550000150.2203141401040000076.Eliminate": 0,
"2311081103550000150.2203141401040000076.Valuable": 3,
"2311081103550000150.2203141401380000076.Death": 2,
"2311081103550000150.2203141401380000076.Eliminate": 2,
"2311081103550000150.2203141401380000076.Valuable": 1,
"2311081103550000150.Subtotal.Death": 11,
"2311081103550000150.Subtotal.Eliminate": 2,
"2311081103550000150.Subtotal.Valuable": 4,
"Total.Death": 13,
"Total.Eliminate": 4,
"Total.Valuable": 5
},
...
{
"PigType": "322",
"PigTypeName": "公猪",
"DataDete": "小计",
"2311011538400000150.2105191446220001076.Death": 1,
"2311011538400000150.2105191446220001076.Eliminate": 0,
"2311011538400000150.2105191446220001076.Valuable": 2,
"2311011538400000150.Subtotal.Death": 1,
"2311011538400000150.Subtotal.Eliminate": 0,
"2311011538400000150.Subtotal.Valuable": 2,
"2311081103550000150.2203141401040000076.Death": 19,
"2311081103550000150.2203141401040000076.Eliminate": 2,
"2311081103550000150.2203141401040000076.Valuable": 1,
"2311081103550000150.2203141401380000076.Death": 0,
"2311081103550000150.2203141401380000076.Eliminate": 0,
"2311081103550000150.2203141401380000076.Valuable": 0,
"2311081103550000150.Subtotal.Death": 19,
"2311081103550000150.Subtotal.Eliminate": 2,
"2311081103550000150.Subtotal.Valuable": 1,
"Total.Death": 20,
"Total.Eliminate": 2,
"Total.Valuable": 3
}
]
}
}
接口里面返回的数据格式我第一次看的时候,对于前端开发来说感觉是非常 特别
的,这里不得不讲一下这里面的逻辑了,报表渲染的表格数据都在 data
这个对象里,headerArea
里面是表头第一行的片区数据,headerPigFarm
是表头第二行猪场数据,headerColumn
是表头第三行静态列数据,有效数据字段是用的 Key
和 Name
,bodyData
是表格内容数据,这里面的数据是最 特别
的,每个对象里,通过 分区
,猪场
的 Key
字段拼接了对应的静态列( Death
, Eliminate
, Valuable
) 字段组合成的数据,然后通过 分区
和 Subtotal
组成小计数据,Total.*
是当前行的静态列总计的数据
表格实现
基于接口返回的数据格式,目前来分析,基于数据拼接表格的方式比较好一点,整个表格实现可以分为两部分,一部分是表头,一部分是表体的数据,表格使用的 nz-table
表头实现
在 thead
中对表头三行进行分别处理,对于 片区
等静态单元格进行手动操作,对于动态的 片区数据
使用循环进行处理,并动态处理 colspan
,表头的数据根据不同行的数据,使用不同的数组,三行表头分别进行处理,表头静态动态行列合并渲染如下
html
<thead>
<tr style="background-color: #fafafa">
<th colspan="2" >片区</th>
<th *ngFor="let item of headerAreaSplit" [attr.colspan]="item.colspan">
{{ item.Name }}
</th>
<th colspan="3" rowspan="2">合计</th>
</tr>
<tr style="background-color: #fafafa">
<th rowspan="2" >阶段</th>
<th>猪场</th>
<ng-container *ngFor="let item of headerPigFarm">
<th colspan="3">
{{ item.Name }}
</th>
</ng-container>
</tr>
<tr style="background-color: #fafafa">
<th>日期</th>
<ng-container *ngFor="let item of headerPigFarm">
<th>死亡</th>
<th>无价淘</th>
<th>有价淘</th>
</ng-container>
<th>死亡</th>
<th>无价淘</th>
<th>有价淘</th>
</tr>
</thead>
下面为表头行的数据处理,根据已经设置好的静态列设置动态列的数量,这里还有一个注意点是控制好 colspan
和需要动态循环的那部分数据
js
const { headerArea, headerPigFarm, headerColumn, bodyData } = data;
// 第一行表头数据组装
this.headerArea = headerArea;
this.headerAreaSplit = [];
// @ts-ignore
let arr = structuredClone(headerArea).splice(0, headerArea.length - 1);
arr.forEach((v) => {
let colspan = 0;
headerPigFarm.forEach((v2) => {
if (v2.Key.indexOf(v.Key) > -1) {
colspan++;
}
});
this.headerAreaSplit.push({
Name: v.Name,
Key: v.Key,
colspan: colspan * 3,
});
});
// 第二行表头数据组装
this.headerPigFarm = headerPigFarm;
this.headerColumn = headerColumn;
this.bodyData = bodyData;
表体实现
这里面首先基于阶段(PigTypeName
)列处理数据,统一处理成 key
, value
的形式,然后处理 rowspan
得到同类型阶段值数据实现行合并,猪场数据循环拼接得到猪场下的静态三列数据,然后单独拼接小计,总计的数据。
以下由于静态数据组装很多,删除了部分代码,只保留了整体结构
js
// 表格数据行组装
let dataList = [];
const nameCount = {};
bodyData.forEach((v) => {
let a3 = [];
a3.push({
key: 'PigTypeName',
value: v.PigTypeName,
rowspan: 0,
});
const name = v.PigTypeName;
nameCount[name] = (nameCount[name] || 0) + 1;
if (v.DataDete.indexOf('T') > -1) {
a3.push({
key: 'DataDete',
value: v.DataDete.split('T')[0],
});
headerPigFarm.forEach((v2) => {
a3.push({
key: v2.Key + '.Death',
value: v[v2.Key + '.Death'],
});
...
});
a3.push({
key: 'Total.Death',
value: v['Total.Death'],
});
...
} else {
a3.push({
key: 'DataDete',
value: v.DataDete,
});
if (v.DataDete === '小计') {
headerPigFarm.forEach((v2) => {
a3.push({
key: v2.Key + '.Death',
value: v[v2.Key + '.Death'],
});
a3.push({
key: v2.Key + '.Eliminate',
value: v[v2.Key + '.Eliminate'],
});
a3.push({
key: v2.Key + '.Valuable',
value: v[v2.Key + '.Valuable'],
});
});
a3.push({
key: 'Total.Death',
value: v['Total.Death'],
});
...
}
...
}
dataList.push(a3);
});
for (const name in nameCount) {
if (nameCount.hasOwnProperty(name)) {
const i = dataList.findIndex((item) => item[0].value === name);
dataList[i][0].rowspan = nameCount[name];
}
}
this.dataList = dataLis;
表体数据处理好以后,渲染这边就简单很多了,由于静态动态数据都拼在了一起拼好了,直接根据行列数组渲染就行了,单元格(td
)行列的 rowspan
和 colspan
在 js
部分也进行了按需处理,这样就得到了最开始看到的最终效果
html
<tbody>
<tr *ngFor="let data of dataList">
<ng-container *ngFor="let v of data">
<ng-container *ngIf="v.rowspan !== 0">
<td [attr.colspan]="v.colspan" [attr.rowspan]="v.rowspan"><span>{{ v.value }}</span></td>
</ng-container>
</ng-container>
</tr>
</tbody>
提示
colspan 和 rowspan 数字大于1后,对应的行列单元格数量需要减少
写在最后
关于这种客户自定义复杂度较高的报表实现,最复杂的部分可能就是渲染逻辑梳理好以后数据的拼接,当然这个也看前后端的配合情况,如果接口返回的数据格式基于前端数据渲染逻辑的话,可能处理的就比较少了
还有更优雅的实现思路吗?
欢迎大家讨论交流,如果文章感觉有用,随手点个赞再走呗
^_^
🥰🥰微信公众号:草帽Lufei