前言
做了一个业务线上的客户定制化报表需求,因为报表的各种合并行,合并列,静态动态按需拼接数据等一系列操作,日常使用的 帆软报表平台
,公司新推的 自定义报表平台
在实现方面可能存在问题,由于年底事情多,人手也不够,各种任务时间排期还特别紧,经组内前端讨论,我们决定用比较稳妥靠谱的方式,手搓了几个定制化报表,由于之前没有做过这么多的自定义合并行列的表格,这次开发实现过程也是一个探索的过程
最终效果
先看下定制化报表的最终效果
这里简单介绍一下里面的业务逻辑
查询逻辑
基于日期、片区、猪场条件查询数据,日期必选,片区和猪场如果没有选择,默认查询所有猪场的数据,片区和猪场是联动效果,如果选了片区,则过滤当前片区下的所有猪场
数据展示
如上图所示,先看报表标题头部,一共三行,前面片区静态,后面合计静态,中间的片区,猪场,小计数据为动态,猪场下面对应的三列数据是静态
接下看表格数据区域,阶段列中的数据是根据查询结果动态显示,同类型需要合并行,平均存栏和死淘率这两个行数据需要合并列,其他数据默认不合并
思路分析
当前定制化报表的需求清楚了,来分析一下实现思路,首先这个报表不是普通的表格,不能直接进行数据绑定,动态标题头和内容需要数据单独拼接,同时需要数据合理处理报表中行列的动态静态合并
实现过程
返回接口数据格式
{ "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
,表头的数据根据不同行的数据,使用不同的数组,三行表头分别进行处理,表头静态动态行列合并渲染如下
<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
和需要动态循环的那部分数据
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
得到同类型阶段值数据实现行合并,猪场数据循环拼接得到猪场下的静态三列数据,然后单独拼接小计,总计的数据。
以下由于静态数据组装很多,删除了部分代码,只保留了整体结构
// 表格数据行组装 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
部分也进行了按需处理,这样就得到了最开始看到的最终效果
<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后,对应的行列单元格数量需要减少
写在最后
关于这种客户自定义复杂度较高的报表实现,最复杂的部分可能就是渲染逻辑梳理好以后数据的拼接,当然这个也看前后端的配合情况,如果接口返回的数据格式基于前端数据渲染逻辑的话,可能处理的就比较少了
还有更优雅的实现思路吗?