1. 看一下需要导出什么样子的表格
如图所示,这里的所有数据行都是动态的,需要根据查询出来的数据循环展示。
如果只是这样的话,使用freemarker应该都可以搞定,但是他一列中内容相同的单元格,需要合并。
这对于表格样式固定的freemarker就搞不定了。
经过一通百度,发现了一个导出文档很好用的框架 poi-tl(实际用的时候也并不怎么好用,学习成本高,功能全)
下面上实战
2. 引入poi-tl 的相关依赖
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.1</version>
</dependency>
关于版本问题,老版比新版好用,新版太过规范
官方文档地址
3.先放一个docx的模版,模版样子如下
生产装置下面哪一行小字是:{{templateRowRenderData}},用来填充数据的,使用双花括号标记。
放在根目录下面,不然找不到哦
3.开始建立实体类,查询数据,填充数据,渲染模版
实体类,如果类中有什么字典,数字标识字段需要转成所表示的字符串
package com.ruoyi.prevention.inventory.domain.vo;
import lombok.Data;
/**
* 安全风险管控措施对象 prevention_risk_measures
*
* @author ruoyi
* @date 2023-06-27
*/
@Data
public class AllInventoryVo {
private String id;
/**
* 管控对象(分析对象名称)
*/
private Integer zoneType;
private String zoneTypeStr;
/**
* 责任部门名称
*/
private String responsibleDepartmentName;
/**
* 责任人名称
*/
private String responsibleStaffName;
/**
* 风险分析单元名称
*/
private String riskUnitName;
/**
* 风险单元id
*/
private String riskUnitId;
/**
* 安全风险事件
*/
private String riskEventName;
/** 风险事件id */
private String riskEventId;
/** 管控措施分类 1 */
private String controlMeasuresClassify1;
/** 管控措施分类 2 */
private String controlMeasuresClassify2;
/** 管控措施分类 3 */
private String controlMeasuresClassify3;
/** 管控措施描述 */
private String controlMeasuresDescription;
/** 隐患排查内容 */
private String treacherousContent;
/** 管控措施id */
private String riskMeasuresId;
/** 岗位负责人 */
private String postResponsible;
/** 巡检周期 */
private Integer checkCycle;
/** 巡检周期单位 */
private Integer checkCycleUnit;
private String checkCycleUnitStr;
}
渲染数据到templateRowRenderData,这里和模版的参数名要保持一致
package com.ruoyi.prevention.inventory.word;
import com.deepoove.poi.expression.Name;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author: fanbaolin
* @Date: 2023/12/12
* @Description: 基础数据+动态数据即
* @Version: 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TemplateData {
@Name("templateRowRenderData")
private TemplateRowRenderData templateRowRenderData;
}
定义哪一列需要填充哪一个字段的值
package com.ruoyi.prevention.inventory.word;
import com.deepoove.poi.data.CellRenderData;
import com.deepoove.poi.data.ParagraphRenderData;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.data.style.CellStyle;
import com.deepoove.poi.data.style.ParagraphStyle;
import com.deepoove.poi.data.style.RowStyle;
import com.deepoove.poi.data.style.Style;
import com.ruoyi.prevention.inventory.domain.vo.AllInventoryVo;
import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author: fanbaolin
* @Date: 2023/12/12
* @Description: 将实体类转化为一个表格 因为渲染只接受RowRenderData类型
* ps:新版真的难用
* @Version: 1.0
*/
@Data
public class TemplateRowRenderData {
/**
* 管控分类措施
*/
private List<RowRenderData> typeRowRenderDataList;
private RowStyle rowStyle;
public TemplateRowRenderData(List<AllInventoryVo> inventoryVos) {
// 初始化样式
initStyle();
// 初始化动态数据
initData(inventoryVos);
}
private void initStyle() {
// 字体样式
Style style = new Style("宋体", 10);
// 段落样式
ParagraphStyle paragraphStyle = new ParagraphStyle();
paragraphStyle.setDefaultTextStyle(style);
// ps:这里才是字体居中对齐
paragraphStyle.setAlign(ParagraphAlignment.CENTER);
// 表格样式
CellStyle cellStyle = new CellStyle();
// ps:表格也需要居中,否则字体不在正中间,会偏上
cellStyle.setVertAlign(XWPFTableCell.XWPFVertAlign.CENTER);
cellStyle.setDefaultParagraphStyle(paragraphStyle);
// 行样式
this.rowStyle = new RowStyle();
rowStyle.setDefaultCellStyle(cellStyle);
}
private void initData(List<AllInventoryVo> inventoryVos) {
// 管控分类
List<RowRenderData> newTypeRowRenderDataList = new ArrayList<>();
if (CollectionUtils.isNotEmpty(inventoryVos)) {
for (AllInventoryVo inventoryVo : inventoryVos) {
// 共有14列
List<CellRenderData> cellDataList = new ArrayList<>();
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getZoneTypeStr())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getResponsibleDepartmentName())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getResponsibleStaffName())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getRiskUnitName())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getRiskEventName())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getControlMeasuresClassify1())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getControlMeasuresClassify2())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getControlMeasuresClassify3())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getControlMeasuresDescription())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getTreacherousContent())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getPostResponsible())));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getCheckCycle() == null ? "":inventoryVo.getCheckCycle() + "")));
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText(inventoryVo.getCheckCycleUnitStr())));
// 备注先空着
cellDataList.add(new CellRenderData().addParagraph(new ParagraphRenderData().addText("")));
RowRenderData rowRenderData = new RowRenderData();
// 样式
rowRenderData.setRowStyle(rowStyle);
rowRenderData.setCells(cellDataList);
newTypeRowRenderDataList.add(rowRenderData);
}
this.typeRowRenderDataList = newTypeRowRenderDataList;
}else{
this.typeRowRenderDataList = Collections.emptyList();
}
}
}
渲染数据并合并列,这里我的数据是摊平的,用了两个指针,首指针和尾指针找同一列上数据相同挨着的单元格然后把它们合并
package com.ruoyi.prevention.inventory.word;
import com.deepoove.poi.data.RowRenderData;
import com.deepoove.poi.policy.DynamicTableRenderPolicy;
import com.deepoove.poi.policy.TableRenderPolicy;
import com.deepoove.poi.util.TableTools;
import lombok.NoArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import java.util.List;
/**
* @author: fanbaolin
* @Date: 2023/12/12
* @Description: 自定义渲染插件-for循环
* 这里因为需要操作表格-所以集成DynamicTableRenderPolicy:动态表格插件,允许直接操作表格对象
* 详情请看:http://deepoove.com/poi-tl/#_%E9%BB%98%E8%AE%A4%E6%8F%92%E4%BB%B6
* @Version: 1.0
*/
@NoArgsConstructor
public class TemplateTableRenderPolicy extends DynamicTableRenderPolicy {
private XWPFTable xwpfTable;
@Override
public void render(XWPFTable xwpfTable, Object data) throws Exception {
if (null == data) {
return;
}
this.xwpfTable = xwpfTable;
TemplateRowRenderData templateRowRenderData = (TemplateRowRenderData) data;
// 分类和管控措施的数据
List<RowRenderData> typeRowRenderDataList = templateRowRenderData.getTypeRowRenderDataList();
if (CollectionUtils.isNotEmpty(typeRowRenderDataList)) {
// 表头下面那一行
int typeMemberRow = 1;
// 移除空白的表头下面那一行
xwpfTable.removeRow(typeMemberRow);
// 得到表头那一行的数据
XWPFTableRow xwpfTableRow = xwpfTable.getRow(0);
for (int i = typeRowRenderDataList.size() - 1; i > -1; i--) {
// 重新插入表格
XWPFTableRow insertNewTableRow = xwpfTable.insertNewTableRow(typeMemberRow);
// 统一高度
insertNewTableRow.setHeight(xwpfTableRow.getHeight());
for (int j = 0; j < 14; j++) {
insertNewTableRow.createCell();
}
// 渲染数据
TableRenderPolicy.Helper.renderRow(xwpfTable.getRow(typeMemberRow), typeRowRenderDataList.get(i));
}
// 合并行 下标为1的行开始合并(去除表头)必须一个一个catch
catchMergeRow(typeRowRenderDataList,0);
catchMergeRow(typeRowRenderDataList,1);
catchMergeRow(typeRowRenderDataList,2);
catchMergeRow(typeRowRenderDataList,3);
catchMergeRow(typeRowRenderDataList,4);
catchMergeRow(typeRowRenderDataList,5);
catchMergeRow(typeRowRenderDataList,6);
catchMergeRow(typeRowRenderDataList,7);
}
}
private void catchMergeRow(List<RowRenderData> typeRowRenderDataList, int cell){
try {
mergeRow(typeRowRenderDataList,cell,1,1,false);
}catch (RuntimeException ignore){
}
}
/**
* 首尾指针递归判断列表下一个值是否和自己相同
*
* @param typeRowRenderDataList
* @param cell
* @param from
* @param to
* @param hasDef
*/
private void mergeRow(List<RowRenderData> typeRowRenderDataList, int cell, int from, int to, boolean hasDef) {
if(from == typeRowRenderDataList.size()){
throw new RuntimeException("跳出递归");
}else{
for (int i = from - 1 ; i < typeRowRenderDataList.size() - 1; i++) {
String content = typeRowRenderDataList.get(i).getCells().get(cell).getParagraphs().get(0).getContents().toString();
String nextContent = typeRowRenderDataList.get(i + 1).getCells().get(cell).getParagraphs().get(0).getContents().toString();
if(nextContent.equals(content)){
to = to + 1;
}else{
if(from > to){
return;
}
if(from == to){
// 整体下移一个单位
from += 1;
to += 1;
}else{
// 合并行
TableTools.mergeCellsVertically(xwpfTable, cell, from, to);
// 合并完成 首指针指向尾端
from = to;
}
// 递归调用
mergeRow(typeRowRenderDataList,cell,from,to,true);
}
}
}
// 如果这一列没有不同的值 全给他合并了
if(!hasDef){
if(from >= to){
return;
}
TableTools.mergeCellsVertically(xwpfTable, cell, from, to);
}
}
}
4.service层掉用
这里我还做了一个word转pdf的操作(用的aspose),没有需求的老铁可以不用要
/**
* 导出安全风险清单
*/
@Override
public void report() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = requestAttributes.getResponse();
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=" + "report.pdf");
List<AllInventoryVo> allInventoryVos = flatList();
clearTreacherousContentLabel(allInventoryVos);
TemplateRowRenderData templateRowRenderData3 = new TemplateRowRenderData(allInventoryVos);
// 2)完整数据
TemplateData templateData = new TemplateData(templateRowRenderData3);
try {
Configure config = Configure.builder().bind("templateRowRenderData", new TemplateTableRenderPolicy()).build();
// 四、导出
ClassPathResource classPathResource = new ClassPathResource("templates" + File.separator + "prevention.docx");
XWPFTemplate template = XWPFTemplate.compile(classPathResource.getInputStream(), config).render(
templateData);
String filePath = "";
if (SystemUtil.getOsInfo().isWindows()) {
filePath = "d:/tmp\\work\\report.doc";
}else{
filePath = "/tmp/work/report.doc";
}
template.writeAndClose(Files.newOutputStream(Paths.get(filePath)));
File file = new File(filePath);
// 生成word filePath是将要被转化的word文档
Document doc = new Document(filePath);
// 转换 字体不一样
doc.save(response.getOutputStream(), SaveFormat.PDF);
// 删除临时文件
file.delete();
} catch (Exception e) {
throw new RuntimeException(e);
}
}