使用XDocReport 导出word
- 效果
- word编辑器
- 案例word模板
- 模板制作
- 解决图片不存在时, "现场照片" 列被隐藏问题
- 依赖
- 返回数据对象 DailyRecord
- ReportOpinionVO 审核记录对象
- PicVo 图片对象
- 导出接口
效果
说明: “现场图片” 为动态图片列表 , “专业负责人审核意见” 和 “项目负责人审核意见” 两行是对象列表数据
word编辑器
使用的WPS
案例word模板
模板制作
-
在需要插入动态数据的位置,按键盘 Ctrl + F9 生成域
-
右键点击上面的域,选择“编辑域”
-
选择“邮件合并” ,再在域代码处 输入对应字段 ${contractName}
-
对象列表数据的处理: ${对象.属性} ,如 ${opinions.opinion}
-
图片数据处理: 先 插入 任意图片,作为模板图片,再点击书签 ,输入书签名,并点击 “添加” 按钮
-
由于我的图片列表数据,在图片所在行依次添加 如下两个域
"@before-row[#list picList as item]"
说明: picList as item ,其中 picList 对应代码中的列表数据, item为每项数据
"@after-row[/#list]"
- 效果如下
解决图片不存在时, “现场照片” 列被隐藏问题
- 在对应的单元格中再加一个单元格, 将图片和对应遍历用的两个域放在其中, 并编辑图片布局,如下
依赖
<!-- 导出word相关 start-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls</artifactId>
<version>2.6.0</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-poi</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.core</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.document</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.document.docx</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.template.freemarker</artifactId>
<version>2.0.2</version>
</dependency>
<!-- 导出word相关 end-->
返回数据对象 DailyRecord
package com.fillke.biz.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.baomidou.mybatisplus.annotation.*;
import com.fillke.biz.vo.ReportOpinionVO;
import com.fillke.common.annotation.Excel;
import com.fillke.common.core.domain.VerifyBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* 跟审日志对象 c_daily_record
*
* @author qts
* @date 2022-11-29
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("c_daily_record")
public class DailyRecord extends VerifyBaseEntity implements Serializable{
private static final long serialVersionUID = 1L;
public static final String FlowKey = "gsrbsp";
/** 日志ID */
@TableId(value = "record_id" ,type = IdType.AUTO)
private Long recordId;
/** 动态投资ID(对应工程名称) */
private Long investId;
/** 日期 */
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd" )
@Excel(name = "日期", width = 30, dateFormat = "yyyy-MM-dd",sort = 1)
private Date recordDate;
/** 天气 */
@Excel(name = "天气",sort = 2)
private String weather;
/** 当日驻场人员,多选逗号分割 */
@Excel(name = "当日驻场人员",sort = 3)
private String personnels;
/** 施工内容 */
//@Excel(name = "施工内容")
private String constructionContent;
/** 主要会议 */
//@Excel(name = "主要会议")
private String majorMeeting;
/** 现场跟踪情况 */
//@Excel(name = "现场跟踪情况")
private String siteCase;
/** 跟审过程问题及追踪 */
//@Excel(name = "跟审过程问题及追踪")
private String issueTrace;
/** 备注 */
private String remarks;
/** 附件 */
//@Excel(name = "附件")
private String files;
/** 删除标志(0代表存在 2代表删除) */
@TableLogic
private String delFlag;
/** 项目ID */
private Long projectId;
/**
* 图片 json数据
*/
private String pictures;
/**
* 其他
*/
private String other;
/**
* 主键
*/
public Long getPk() {
return recordId;
}
/**
* 工程名称(合同名称)
* select = false 解决查询详情时,自动拼接该字段,而当前数据库没有此字段,从而报错
*/
@TableField(insertStrategy = FieldStrategy.NEVER,updateStrategy = FieldStrategy.NEVER,select = false)
@Excel(name = "工程名称",sort = 0, needMerge = true)
private String contractName;
/**
* 创建者
*/
@Excel(name = "创建人",sort = 101, needMerge = true)
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 102, needMerge = true)
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新者
*/
@Excel(name = "更新人",sort = 103, needMerge = true)
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 104, needMerge = true)
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 创建人id
*/
@TableField(fill = FieldFill.INSERT)
private String createId;
/**
* 更新人id
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateId;
// =============== 非映射字段 start ========================
/**
* 查询状态
*/
@TableField(exist = false)
private List<Integer> queryStatus;
/**
* 项目负责人
*/
@TableField(exist = false)
private String projectLeaderName;
/**
* 审核人意见列表
*/
@TableField(exist = false)
private List<ReportOpinionVO> opinions;
// =============== 非映射字段 end ========================
}
ReportOpinionVO 审核记录对象
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* desc:
*
* @author qts
* @date 2023/1/11 0011
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReportOpinionVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节点名称
*/
private String nodeName;
/**
* 审核意见
*/
private String opinion;
}
PicVo 图片对象
import fr.opensagres.xdocreport.document.images.ByteArrayImageProvider;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* desc:
*
* @author qts
* @date 2023/1/11 0011
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PicVo implements Serializable {
private static final long serialVersionUID = 1L;
private ByteArrayImageProvider pic;
}
导出接口
/**
* 导出word
*/
@Log(title = "跟审日志", businessType = BusinessType.EXPORT)
@PostMapping("/exportWord")
public void exportWord(HttpServletResponse response, Long recordId) {
InputStream in = null;
FileOutputStream out =null;
try {
//获取本地目录的word模板
in = new FileInputStream("D:\\跟审导出模板\\跟审日志模板.docx");
//注册xdocreport实例并加载FreeMarker模板引擎
IXDocReport report = XDocReportRegistry.getRegistry().loadReport(in, TemplateEngineKind.Freemarker);
//创建xdocreport上下文对象
IContext context = report.createContext();
// 封装返回需要数据 start
DailyRecord dailyRecord = dailyRecordService.getById(recordId);
DynamicInvest dynamicInvest = dynamicInvestService.getById(dailyRecord.getInvestId());
// 设置工程名称
dailyRecord.setContractName(dynamicInvest.getContractName());
TProject project = projectService.getById(dailyRecord.getProjectId());
// 设置项目负责人名称
dailyRecord.setProjectLeaderName(Optional.ofNullable(project).map(BaseEntity::getCreateBy).orElse(""));
List<ReportOpinionVO> opinionList = CollUtil.newArrayList();
// 根据流程key获取所有节点名称
List<ProcessNode> nodes = bpmDefinitionService.getNodes(DailyRecord.FlowKey);
// 根据实例ID查询历史审核记录
List<TaskOpinion> opinions = CollUtil.newArrayList();
if (StrUtil.isNotBlank(dailyRecord.getInstId())) {
opinions = instanceService.getOpinions(dailyRecord.getInstId());
}
// 处理审批意见列表
for (int i = 0; i < nodes.size(); i++) {
if (i < 2 || i >= nodes.size() - 1) {
continue;
}
// 节点名称
String nodeName = nodes.get(i).getNodeName();
// 节点审核意见
List<String> opinionStrs = opinions.stream().filter(e -> nodeName.equals(e.getTaskName())).map(TaskOpinion::getOpinion).collect(Collectors.toList());
String opinion = opinionStrs.size() > 0 ? opinionStrs.get(opinionStrs.size() - 1) : "";
ReportOpinionVO opinionMap = new ReportOpinionVO(nodeName + "意见",Optional.ofNullable(opinion).orElse(""));
opinionList.add(opinionMap);
}
// 设置审核意见列表
dailyRecord.setOpinions(opinionList);
// 封装返回需要数据 end
//将需要替换的数据数据添加到上下文中
//其中key为word模板中的域名,value是需要替换的值
context.put(ColumnUtil.getName(DailyRecord::getContractName), Optional.ofNullable(dailyRecord.getContractName()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getRecordDate), Optional.ofNullable(dailyRecord.getRecordDate()).map(DateUtil::formatDate).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getWeather), Optional.ofNullable(dailyRecord.getWeather()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getPersonnels), Optional.ofNullable(dailyRecord.getPersonnels()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getConstructionContent), Optional.ofNullable(dailyRecord.getConstructionContent()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getMajorMeeting), Optional.ofNullable(dailyRecord.getMajorMeeting()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getSiteCase), Optional.ofNullable(dailyRecord.getSiteCase()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getIssueTrace), Optional.ofNullable(dailyRecord.getIssueTrace()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getOther), Optional.ofNullable(dailyRecord.getOther()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getOpinions),dailyRecord.getOpinions());
context.put(ColumnUtil.getName(DailyRecord::getCreateBy), Optional.ofNullable(dailyRecord.getCreateBy()).orElse(""));
context.put(ColumnUtil.getName(DailyRecord::getProjectLeaderName), Optional.ofNullable(dailyRecord.getProjectLeaderName()).orElse(""));
//创建字段元数据,需要表格才加下面这两行,否则不用
FieldsMetadata fm = report.createFieldsMetadata();
//Word模板中的表格数据对应的集合类型
fm.load(ColumnUtil.getName(DailyRecord::getOpinions), ReportOpinionVO.class, true);
// 设置特殊字段: 图片 ,pic对应书签 item.pic对应每一项PicVo中的pic字段
fm.addFieldAsImage("pic", "item.pic", NullImageBehaviour.RemoveImageTemplate);
// 图片列表
String pictures = dailyRecord.getPictures();
List<PicVo> picList = CollUtil.newArrayList();
if (StrUtil.isNotBlank(pictures)) {
JSONArray picJsonArray = JSONArray.parse(pictures);
if (CollUtil.isNotEmpty(picJsonArray)) {
for (int i = 0; i < picJsonArray.size(); i++) {
JSONObject picJsonObject = picJsonArray.getJSONObject(i);
String url = picJsonObject.get("url").toString();
// 本地资源路径
String localPath = FillkeConfig.getProfile();
// 数据库资源地址
String downloadPath = localPath + StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX);
//fm.addFieldAsImage("pic"+num);
PicVo picVo = new PicVo(new ByteArrayImageProvider(new FileInputStream(downloadPath)));
picList.add(picVo);
}
}
}
context.put("picList", picList); // 图片数据存入上下文
//输出到本地目录
//out = new FileOutputStream(new File("D://用户模板导出.docx"));
//处理word文档并输出
//report.process(context, out);
//浏览器端下载
response.setCharacterEncoding("utf-8");
response.setContentType("application/msword");
String fileName = "跟审日志.docx";
fileName = new String(fileName.getBytes("UTF-8"), "ISO8859-1");// 谷歌
response.setHeader("Content-Disposition", "attachment;filename="+fileName);
report.process(context, response.getOutputStream());
} catch (IOException e) {
logger.error("读取Word模板异常",e);
} catch (XDocReportException e) {
logger.error("word模板生成失败",e);
} finally {
if(in!=null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out!=null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
参考:https://www.cnblogs.com/huigee/p/16588247.html
https://blog.csdn.net/weixin_45433031/article/details/125874897