最近项目中遇到一个需要解析Excel并在线展示的功能,excel的内容不是固定的,是需要事先通过专用的工具生成xml模板导入到系统中,然后系统解析上报的excel文件时就可以根据模板来解析内容。
其中生成的xml模板保存了针对每一个单元格的一些属性,所以excel解析不是难事,关键是页面展示excel内容的时候有个合并单元格的需求,这个着实让我苦恼了几天,上下班的路上都在思考这个问题,后来终于想到一种方案,这里记录一下。
因为excel内容不是固定的,所以解析与展示数据时,我都是以单元格为单位的,所以这里我的实体类对象是:
public class TemplateItem {
private Long id;
/** 列位置 **/
private String col;
/** 列合并数 **/
private String colSpan;
/** 行位置 **/
private String row;
/** 行合并数 **/
private String rowSpan;
/** 数据类型(0标签 1指标值 2计算项) **/
private String contentEnum;
/** 指标信息 **/
private String content;
}
返回给前端展示的时候,要么返回每一个单元格,这样直接前端通过<table></table>
标签或者Bootstrap Table
插件画出即可;要么只返回有数据的单元格,需要前端计算出单元格具体的展示位置。
既然是后端开发(虽然前端也是自己写),那我就把计算的逻辑放后端了,而且对比了一下,感觉前端计算好像也不是太方便。然后问题就来了,存在合并单元格的情况时我怎么计算每个单元格的具体位置。首先明确的是,前端直接采用<table></table>
标签来实现,虽然两种都实现了,但是Bootstrap Table
在合并单元格的时候似乎不是很好用,当初采用只是为了样式美观😓,<table></table>
标签则可直接通过<td>
的rowspan、colspan
属性来控制合并的行数和列数。
------------------------------------------------ 重点来了 ------------------------------------------------
使用<table></table>
来画excel的内容,也就是我返回是数据格式得是一个二维数组,因为<table></table>
标签是使用<tr>、<td>
来画表格的,这样我数组里每一项放那个单元格对象即可。
在组装二维数组的时候,一开始我总是想着在没有数据的位置拼上空的单元格对象,但是感觉计算好复杂,因为合并多行多列的情况都有,这样计算的时候要考虑很多。后来一想,干脆我反过来做,因为已经知道一共有多少行多少列了,我先按照一张空白excel的样子,先画出每一个单元格(先组装一个空的二维数组),然后根据已有的数据项信息,删除掉被合并的单元格(删除被合并的数组项),这样不就能很容易得出来想要的二维数组了吗。
上代码:
两个实体类Template
和TemplateItem
,其中Template
存储了这一套模板的属性信息,TemplateItem
存储了模板里每一个单元格的属性信息。
Template
对象:
package com.example.demo.excel;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 模板
*
* @author zzs
* @date 2021/12/7 17:16
*/
@Getter
@Setter
public class Template implements Serializable {
private Long id;
/** 模板名称 **/
private String templateName;
/** 行数 **/
private String rowCount;
/** 列数 **/
private String colCount;
/** 表头行数 **/
private Integer headCount;
/** 备注 **/
private String remark;
private Boolean isDel;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
/** 模板项列表 **/
private List<TemplateItem> items;
private static final long serialVersionUID = 1L;
}
TemplateItem
对象:
package com.example.demo.excel;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
/**
* 模板项
*
* @author zzs
* @date 2021/12/7 17:11
*/
@Getter
@Setter
public class TemplateItem implements Serializable {
private Long id;
/** 模板id **/
private Long templateId;
/** 列位置 **/
private String col;
/** 列合并数 **/
private String colSpan;
/** 行位置 **/
private String row;
/** 行合并数 **/
private String rowSpan;
/** 数据类型(0标签 1指标值 2计算项) **/
private String contentEnum;
/** 指标信息 **/
private String content;
/** 公式 **/
private String expstr;
/** 是否特指单位 **/
private String isUnitConver;
/** 特指单位 **/
private String eleUnit;
/** 自定义详细样式 **/
private String cssClass;
/** 自定义格式化 **/
private String formatString;
/** 是否只读 **/
private String isReadonly;
private static final long serialVersionUID = 1L;
}
业务方法:
其中二维数组我是用guava
的Table
来代替,这个使用起来比较方便。
package com.example.demo.excel;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author zzs
* @date 2021/12/7 17:12
*/
@Service
public class TemplateService {
/**
* 入口
*/
public List<List<TemplateItem>> getFormData() {
// 这里就不展示数据的获取了
Template template = new Template();
List<TemplateItem> items = template.getItems();
HashBasedTable<Integer, Integer, TemplateItem> dataTable = HashBasedTable.create();
items.stream().forEach(item -> {
Integer row = Integer.valueOf(item.getRow());
Integer col = Integer.valueOf(item.getCol());
dataTable.put(row, col, item);
});
Integer rowCount = Integer.valueOf(template.getRowCount());
Integer colCount = Integer.valueOf(template.getColCount());
List<List<TemplateItem>> dataList = fillTableCell(rowCount, colCount, dataTable);
return dataList;
}
/** 填充表格 **/
private List<List<TemplateItem>> fillTableCell(int rowCount, int colCount,
Table<Integer, Integer, ? extends TemplateItem> dataTable) {
// 初始化一个空的table
HashBasedTable<Integer, Integer, TemplateItem> fullTable = HashBasedTable.create();
for (int i = 1; i <= rowCount; i++) {
for (int j = 1; j <= colCount; j++) {
fullTable.put(i, j, new TemplateItem());
}
}
// 排序
List<List<? extends TemplateItem>> dataList = dataTable.rowMap().entrySet().stream()
.sorted(Comparator.comparing(Map.Entry::getKey))
.map(entry -> entry.getValue().entrySet().stream()
.sorted(Comparator.comparing(Map.Entry::getKey))
.map(Map.Entry::getValue)
.collect(Collectors.toList()))
.collect(Collectors.toList());
// 将实际的值存储到table中的cell
for (List<? extends TemplateItem> rowData : dataList) {
for (TemplateItem templateItem : rowData) {
Integer row = Integer.valueOf(templateItem.getRow());
Integer col = Integer.valueOf(templateItem.getCol());
Integer rowSpan = Integer.valueOf(templateItem.getRowSpan());
Integer colSpan = Integer.valueOf(templateItem.getColSpan());
fullTable.put(row, col, templateItem);
// 判断要合并的cell
if (rowSpan == 1 && colSpan == 1) {
continue;
}
if (rowSpan > 1 && colSpan == 1) {
for (int i = 1; i < rowSpan; i++) {
fullTable.remove(row + i, col);
}
continue;
}
if (rowSpan == 1 && colSpan > 1) {
for (int i = 1; i < colSpan; i++) {
fullTable.remove(row, col + i);
}
continue;
}
if (rowSpan > 1 && colSpan > 1) {
for (int i = 0; i < rowSpan; i++) {
for (int j = 0; j < colSpan; j++) {
if (i == 0 && j == 0) {
continue;
}
fullTable.remove(row + i, col + j);
}
}
continue;
}
}
}
// 转换为输出的格式
return fullTable.rowMap().entrySet().stream()
.map(rowEntry -> rowEntry.getValue().entrySet().stream()
.map(colEntry -> colEntry.getValue())
.collect(Collectors.toList()))
.collect(Collectors.toList());
}
}
这样返回的集合在前端直接遍历画出每一行<tr>
每一个<td>
即可。