基于Poi-tl导出Word(含行、列合并)
poi-tl是一个基于Apache POI的Word模板引擎,也是一个免费开源的Java类库,你可以非常方便的加入到你的项目中,并且拥有着让人喜悦的特性。
—官方文档
各种导出word方案的对比
前提
Maven
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
历史版本关联
模版的描述
话不多说,直接上代码
@Slf4j
public class ServerTablePolicy extends DynamicTableRenderPolicy {
/**
* 填充数据所在行数
*/
private final int laborsStartRow;
/**
* 表格的列数
*/
private final int headerColumnIndex;
/**
* 合并的列
*/
private List<Integer> colArr;
/**
* 表格行高
*/
private int height = 250;
/**
* 是否启用水平合并(列合并)
*/
private boolean enableHorizontalMerge = false;
/**
* 是否启用垂直合并(行合并)
*/
private boolean enableVerticalMerge = true;
// 存储列对齐方式
private Map<Integer, ParagraphAlignment> columnAlignments = new HashMap<>();
public ServerTablePolicy(int laborsStartRow, int headerColumnIndex) {
this.laborsStartRow = laborsStartRow;
this.headerColumnIndex = headerColumnIndex;
}
public void setColArr(List<Integer> colArr) {
this.colArr = colArr;
}
public void setHeight(int height) {
this.height = height;
}
public void setEnableHorizontalMerge(boolean enableHorizontalMerge) {
this.enableHorizontalMerge = enableHorizontalMerge;
}
public void setEnableVerticalMerge(boolean enableVerticalMerge) {
this.enableVerticalMerge = enableVerticalMerge;
}
/**
* 设置指定列的对齐方式
* @param columnIndex 列索引
* @param alignment 对齐方式
*/
public void setColumnAlignment(ParagraphAlignment alignment, int... columnIndex) {
for (int index : columnIndex) {
columnAlignments.put(index, alignment);
}
}
@Override
public void render(XWPFTable xwpfTable, Object tableData) throws Exception {
if (null == tableData) {
return;
}
ServerTableData serverTableData = (ServerTableData) tableData;
List<RowRenderData> serverDataList = serverTableData.getServerDataList();
if (CollectionUtils.isNotEmpty(serverDataList)) {
// 先删除一行, demo中第一行是为了调整 三线表 样式
xwpfTable.removeRow(laborsStartRow);
// 行从中间插入, 因此采用倒序渲染数据
for (int i = serverDataList.size() - 1; i >= 0; i--) {
XWPFTableRow newRow = xwpfTable.insertNewTableRow(laborsStartRow);
newRow.setHeight(height);
for (int j = 0; j < headerColumnIndex; j++) {
newRow.createCell();
}
// 渲染一行数据
TableRenderPolicy.Helper.renderRow(newRow, serverDataList.get(i));
// 设置列对齐方式
for (int j = 0; j < headerColumnIndex; j++) {
if (columnAlignments.containsKey(j)) {
XWPFTableCell cell = newRow.getCell(j);
ParagraphAlignment alignment = columnAlignments.get(j);
cell.getParagraphs().forEach(paragraph -> paragraph.setAlignment(alignment));
}
}
// 水平合并(列合并)
if (enableHorizontalMerge) {
int startCol = 0;
while (startCol < headerColumnIndex - 1) {
String currentValue = serverDataList.get(i).getCells().get(startCol)
.getParagraphs().get(0).getContents().get(0).toString();
int endCol = startCol + 1;
// 向后查找连续相同的值
while (endCol < headerColumnIndex) {
String nextValue = serverDataList.get(i).getCells().get(endCol)
.getParagraphs().get(0).getContents().get(0).toString();
if (!currentValue.equals(nextValue)) {
break;
}
endCol++;
}
// 如果找到连续相同的值,进行合并
if (endCol > startCol + 1) {
TableTools.mergeCellsHorizonal(xwpfTable, laborsStartRow, startCol, endCol - 1);
// 加粗处理
XWPFTableCell cell = newRow.getCell(startCol);
if (cell != null && cell.getParagraphArray(0) != null) {
List<XWPFRun> runs = cell.getParagraphArray(0).getRuns();
for (XWPFRun run : runs) {
run.setBold(true);
}
}
}
startCol = endCol;
}
}
}
// 垂直合并(行合并)
if (enableVerticalMerge && colArr != null && !colArr.isEmpty()) {
for (Integer colIndex : colArr) {
if (colIndex >= headerColumnIndex) {
continue; // 跳过超出范围的列
}
List<String> strings = new ArrayList<>();
// 收集该列的所有值
for (int i = 0; i < serverDataList.size(); i++) {
try {
String value = serverDataList.get(i).getCells().get(colIndex)
.getParagraphs().get(0).getContents().get(0).toString();
strings.add(value != null ? value : "");
} catch (Exception e) {
log.error("获取单元格值时发生错误: row={}, col={}", i, colIndex, e);
strings.add(""); // 添加空值作为占位符
}
}
// 检查收集的数据是否为空
if (strings.isEmpty()) {
continue;
}
// 获取需要合并的行组
Map<String, List<List<Integer>>> result = ConsecutiveDoublesUtil.getConsecutiveDoubles(strings);
// 执行合并
if (result != null && !result.isEmpty()) {
for (Map.Entry<String, List<List<Integer>>> entry : result.entrySet()) {
for (List<Integer> integers : entry.getValue()) {
if (integers != null && integers.size() > 1) {
try {
TableTools.mergeCellsVertically(xwpfTable, colIndex,
integers.get(0) + laborsStartRow,
integers.get(integers.size() - 1) + laborsStartRow);
} catch (Exception e) {
log.error("垂直合并单元格时发生错误: col={}, fromRow={}, toRow={}",
colIndex, integers.get(0), integers.get(integers.size() - 1), e);
}
}
}
}
}
}
}
}
}
}
以上代码段是基于官方提供的动态循环生成表格插件编写的自定义插件;主要实现:
1、调用时可指定是否开启行/列合并
2、指定都需要哪些行/列进行合并
3、合并时的逻辑为相邻行/列值相同时,自动合并
public static void exportWord(HttpServletResponse response, List<CommonExportVo> commonExportVos) throws IOException {
Map<String, Object> map = new HashMap<>();
ServerTableData tableData = new ServerTableData();
List<RowRenderData> tableDataList = new ArrayList<>(); // 初始化传入表格的行列表数据结构
for (CommonExportVo tableDatum : commonExportVos) { // 为每行赋值
tableDataList.add(Rows.of(一列值, 二列值, 三列值, 四列值, 五列值, 六列值, 七列值)
.textFontFamily("宋体").textFontSize(12).center().create());
}
tableData.setServerDataList(tableDataList); // 设置表格数据
map.put("tableData", tableData);
// 初始化填充表格策略,从第二行开始填充,一共有7列数据;默认开启同一列如果存在连续的相同数据进行【行合并】,关闭列合并
ServerTablePolicy tableServerPolicy = new ServerTablePolicy(2, 7);
tableServerPolicy.setColumnAlignment(ParagraphAlignment.LEFT, 3, 4);
tableServerPolicy.setColArr(Arrays.asList(0)); // 指定需要垂直合并的列
/** 2、配置策略 */
ConfigureBuilder builder = Configure.builder().useSpringEL(false).bind("tableData", tableServerPolicy);
/** 3.渲染文档 */
String templatePath = new ClassPathResource("forms/Template.docx").getFile().getAbsolutePath();
InputStream resourceAsStream = new FileInputStream(new File(templatePath));
XWPFTemplate template = XWPFTemplate.compile(Objects.requireNonNull(resourceAsStream), builder.build()).render(map);
String fileName = "aaa.docx";
// 输出到响应流,设置响应头
// response.setContentType("application/octet-stream");
// response.setHeader("Content-disposition", "attachment;filename=\"" + URLEncoder.encode(fileName, "UTF-8"));
// OutputStream out = response.getOutputStream();
// 输出到本地
String outFilePath = "C:\\Users\\2908\\Desktop\\" + fileName;
OutputStream out = new FileOutputStream(new File(outFilePath));
BufferedOutputStream bos = new BufferedOutputStream(out);
try {
template.write(bos);
bos.flush();
out.flush();
} catch (Exception exception) {
log.error("导出Word失败!", exception);
throw new IOException("导出Word失败!", exception);
} finally {
PoitlIOUtils.closeQuietlyMulti(template, bos, out);
}
}
以上是poi-tl的调用方法。