luckysheet(3):基于 luckysheet 实现自定义报表

4,330 阅读5分钟

我正在参加「掘金·启航计划」

前言

经过 2 个月的设计开发周期,基于 Luckysheet 的自定义报表功能开发总算阶段结束了。

目前的报表使用程度还是比较简单的,但中间涉及到的流程难度是比较大的,现在对其做个总结。

一、概述

问题:自定义报表的需求产生来源?

在很多的内部管理系统里面,都会有些专门的报表模块开发,针对特定的需求,展示一张表固定的字段,然后由程序员进行后端表的设计及数据组装并在前端页面展示的功能。当遇到一些需要动态生成报表的情况时,以及维护一些动态报表的功能时,就最好有一个自定义报表的功能来完成此需求。而自定义报表的设计是非常重要的,根据设计的程度,完成功能的复杂度也相差很大。例如有些报表,固定了一些查询条件,然后勾选这些查询条件就默认是总一张表,这就是简单的需求,而自定义报表需要一些更加复杂的定制功能。

自定义报表优势:

1、自动化:不需要手动收集、汇总数据,录入excel;

2、易操作:设置好后直接查询即可;

3、高价值:发挥起数据分析的作用;

目前版本的自定义报表特点:

优点:

  • 灵活性强;
  • 基于 luckysheet 开发的自定义报表,具备原生的 excel 报表的优势,具有强大的功能,例如生成图表保存等;

缺点:

  • 上手难度比较复杂,自由度高;
  • 比较简约;

二、设计思路

自定义报表的设计思路是大体需要几个概念:

  • 字段:字段可以理解为表的表头数据,一些报表管理系统中的字段是固定的,通过固定的字段查询特定的数据即可,而真正要做自定义,报表的内容即是动态可维护的,并且需要为这些字段赋予特定的搜索条件;
  • 规则:规则则是针对字段亦或表设计的规则,规则可以制定查询时间范围、报表细粒度(日报表、周报表、月报表、年报表、自定义报表)、查询的数据细粒度(分、小时)等、报表数据的填充方向(横向、纵向)等;
  • 表头:可以将报表拆分为两类,一类是日常查询报表,查询日常的数据即可,作为查询使用;另一类是维护报表,运维人员可能需要填入特定的数据,并展示到各个报表的用途;而维护表则需要表头的数据,字段如果在维护表中使用,则升级为表头,这些表头可以在另一个页面渲染展示数据;

当然,主要搞定前两个就可以完成一个自定义报表的雏形了。

在使用 luckysheet 的过程中,发现如果后端将数据返回前端的话,前端把 整个 json 中的 celldata 元素挨个给单元格填充数据赋值的话,就会显得很慢,随着数据量的庞大,渲染的过程可能达到分钟级别,因此,就只能靠后端来完成数据组装工作了,然后前端拼接数据即可。

后端数据组装思路大概如下:

三、实现案例

数据的保存功能依旧通过存储整个大 json 来完成,毕竟简单。

前端向单元格赋值数据的时候可以插入额外的属性信息,告知后端这个单元格的信息是字段信息,代码如下:

    luckysheet.setCellValue(2, 1, {
        m: "字段a",
        v: "字段a",
      fieldId: "1",
   });

后端识别出字段后,往这个字段的下方或右方根据字段规则查询数据,并组装数据,往每个单元格赋值即可。

还需要判别出合并单元格的信息:

private ArrayList<ReportCustomWorkSheetHeader> getReportCustomWorkSheetHeaders(JSONArray jsonArray, List<JSONObject> jsonObjects) {
        ArrayList<ReportCustomWorkSheetHeader> sheetHeaders = new ArrayList<>();
        for (int i = 0; i < jsonObjects.size(); i++) {
            JSONObject jsonObject = (JSONObject) jsonArray.get(i);
            JSONArray celldatas = jsonObject.getJSONArray("celldata");
            List<ReportCustomWorkSheetHeader> collect = (List<ReportCustomWorkSheetHeader>) celldatas.stream().map(e -> {
                ReportCustomWorkSheetHeader header = new ReportCustomWorkSheetHeader();
                JSONObject properties = BeanUtil.copyProperties(e, JSONObject.class);
                header.setRow((Integer) properties.get("r"));
                header.setColumn((Integer) properties.get("c"));
                JSONObject v = (JSONObject) properties.get("v");
                if (v != null) {
                    header.setV((v.get("v")==null?null:v.get("v")+""));
                    header.setM((v.get("m")==null?null:v.get("m")+""));
                    if (v.get("fieldId") != null) {
                        header.setFieldId(Long.valueOf(v.get("fieldId").toString().trim()));
                    }
                    if(v.get("isData") !=null){
                        return null;
                    }
                    header.setCellJson(JSON.toJSONString(v));
                    JSONObject mc = (JSONObject) v.get("mc");
                    if(mc!=null){
                        header.setMcC((Integer) mc.get("c"));
                        header.setMcR((Integer) mc.get("r"));
                        header.setMcRs((Integer) mc.get("rs"));
                        header.setMcCs((Integer) mc.get("cs"));
                    }
                }
                return header;
            }).filter(e->e!=null).collect(Collectors.toList());
            sheetHeaders.addAll(collect);
        }
        return sheetHeaders;
    }

合并单元格中,如果是上下合并,则基准点以最下面的单元格为准;如果是左右合并,则基准点以最右边的单元格为准添加数据。

可得公式如下:

得到数据后,向各个字段的下方或右方赋值:

  private ArrayList<ReportCustomWorkSheetHeader> getResult(ArrayList<ReportCustomWorkSheetHeader> sheetHeaders, LinkedHashMap<String, LinkedHashMap<String, String>> linkedHashMap, ReportCustomWorkSheetRuleSetting allRuleSetting) {
        ArrayList<ReportCustomWorkSheetHeader> insetCellDatas = new ArrayList<>();
        sheetHeaders.stream().filter(sh->sh.getFieldId()!=null).forEach(shs->{
            LinkedHashMap<String, String> valueMap = linkedHashMap.get(shs.getInitFieldId().toString().trim());
            if(CollectionUtil.isNotEmpty(valueMap)){
                int row = shs.getRow();
                int column =shs.getColumn();
                if(shs.getMcC()!=null && shs.getMcCs()!=null){
                    row = shs.getMcRs()+shs.getRow()-1;
                    column = shs.getMcCs()+shs.getColumn() -1;
                }
                for (Map.Entry<String, String> valueEntry : valueMap.entrySet()) {
                    ReportCustomWorkSheetHeader cellData = new ReportCustomWorkSheetHeader();
                    if(allRuleSetting.getFillDirection().equals(ReportConst.REPORT_CUSTOM_FILL_DIRECTION_0.getStatus())){
                        cellData.setRow(row);
                        cellData.setColumn(++column);
                    }else{
                        cellData.setRow(++row);
                        cellData.setColumn(column);
                    }
                    cellData.setIsData(1);
                    cellData.setM(valueEntry.getValue());
                    cellData.setV(valueEntry.getValue());
                    insetCellDatas.add(cellData);
                }
            }
        });
        return insetCellDatas;
    }

然后根据这些数据填充 celldata 数据,返回 json 即可,这是个完整的demo流程设计。

总体来说,功能的复杂度在于流程的判断条件较多,需要考虑日月年分钟等各种情况的出现,完成这部分考虑后,一个简单的自定义报表即完成了,最后,感谢 luckysheet 的开源贡献🥂。

前文回顾: