需求
需要将一个报表的数据导出成一个word,报表中有固定项,还有需要动态生成n个的表单。简单点举个例子差不多就像是下面这样。
最开始想用easy-poi导出至word模板来实现,发现他只能实现固定的项目,像上图中,有N个周期的成绩就要显示n个成绩块的这个就无法实现。
百度了一圈……找啊找不到实现的方式,最后突然想到以前做的导出word是用freemarker实现的,于是稍微研究了一下,就搞定了。
具体实现
一、创建一个word文件
新建一个word文件,然后把想要的样式什么的都调整好,字体、文字大小、行间距等等。
二、将word文件另存为成.xml文件
F12 -> 选择.xml文件 -> 保存
三、直接将.xml文件的后缀改成.ftl
这应该会吧……
四、把.ftl文件放到java项目中去
可以格式化一下,idea支持ftl格式的格式化,格式化之后看起来好看多了
五、编辑.ftl文件
1.咱们先大概的分析一下整个文件的构成
进入body看一下
<w:p>这个标签可以看做是html的p标签,一个自然段的内容会在一个<w:p>标签里面。
<w:tbl>这个就相当于html的table标签,
<w:tr>相当于html的tr标签
<w:tc>这个应该是单元格标签,代表了一个单元格。
<w:p>标签中的实际显示的内容是由<w:r>标签中的<w:t>标签的内容决定的
<w:rpr>这个标签里面的东西是用来改当前这个p标签里面内容的样式的。
2.将占位的值改成freemarker语法的参数
假设咱们的数据格式是这样的:
public class Entity {
private String userName;
private List<Grade> grades;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public List<Grade> getGrades() {
return grades;
}
public void setGrades(List<Grade> grades) {
this.grades = grades;
}
public Entity(String userName, List<Grade> grades) {
this.userName = userName;
this.grades = grades;
}
public Entity() {
}
}
public class Grade {
private String period;
private Double chinese;
private Double math;
private Double english;
public String getPeriod() {
return period;
}
public void setPeriod(String period) {
this.period = period;
}
public Double getChinese() {
return chinese;
}
public void setChinese(Double chinese) {
this.chinese = chinese;
}
public Double getMath() {
return math;
}
public void setMath(Double math) {
this.math = math;
}
public Double getEnglish() {
return english;
}
public void setEnglish(Double english) {
this.english = english;
}
public Grade(String period, Double chinese, Double math, Double english) {
this.period = period;
this.chinese = chinese;
this.math = math;
this.english = english;
}
}
注意:传入模板中的数据,一定要是一个Map,如果有数组,数组也需要转成一个List才行。
freemarker取值语法:${变量名}
成绩单下面的具体的成绩的表格以及周期,是一个集合遍历出来的,咱们也要在ftl文件中将遍历的逻辑写出来
freemarker遍历标签,在需要遍历的区域的最外层,套一个<#list>标签
<#list 数组 as 每个对象别名>
有点像for循环给循环的对象起一个别名
for(Grade grade in grades)
需要取遍历对象的某个属性的值,就要这么写
${别名.属性名}
周期值:
${grade.period}
下面语文、数学、英语上都去替换一下
至此已经可以满足基本的要求了。
3.编写接口和写入word逻辑
controller
@GetMapping("test")
public void test(HttpServletResponse response, HttpServletRequest request) throws IllegalAccessException, IOException {
Entity entity = new Entity();
entity.setUserName("张三");
List<Grade> grades = new ArrayList<>();
grades.add(new Grade("202301", 100D, 100D, 100D));
grades.add(new Grade("202302", 100D, 100D, 100D));
grades.add(new Grade("202303", 100D, 100D, 100D));
entity.setGrades(grades);
String wordName = "test.ftl";
String fileName = "result.docx";
String name = "result";
Map<String, Object> map = ObjectToMap(entity);
WordUtil.exportMillCertificateWord(request, response, map, wordName, fileName, name);
}
WordUtil
package com.ruoyi.system;
import freemarker.template.Configuration;
import freemarker.template.Template;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Map;
/**
* @author Frank
* @date 2023/9/11
*/
public class WordUtil {
//配置信息,代码本身写的还是很可读的,就不过多注解了
private static Configuration configuration = null;
// 这里注意的是利用WordUtils的类加载器动态获得模板文件的位置
//private static final String templateFolder = wordUtils.class.getClassLoader().getResource("../../../../templates").getPath();
private static final String templateFolder = WordUtil.class.getResource("/templates").getPath();
static {
configuration = new Configuration();
configuration.setDefaultEncoding("utf-8");
try {
System.out.println(templateFolder);
configuration.setDirectoryForTemplateLoading(new File(templateFolder));
} catch (IOException e) {
e.printStackTrace();
}
}
private WordUtil() {
throw new AssertionError();
}
/**
* 导出excel
*
* @param request 请求对象
* @param response 响应对象
* @param map word文档中参数
* @param wordName 为模板的名字 例如xxx.ftl
* @param fileName 是word 文件的名字 格式为:"xxxx.doc"
* @param name 是临时的文件夹米名称 string类型 可随意定义
* @throws IOException
*/
public static void exportMillCertificateWord(HttpServletRequest request, HttpServletResponse response, Map map, String wordName, String fileName, String name) throws IOException {
Template freemarkerTemplate = configuration.getTemplate(wordName);
File file = null;
InputStream fin = null;
ServletOutputStream out = null;
try {
// 调用工具类的createDoc方法生成Word文档
file = createDoc(map, freemarkerTemplate, name);
fin = new FileInputStream(file);
response.setCharacterEncoding("utf-8");
response.setContentType("application/x-download");
fileName = new String(fileName.getBytes(), "ISO-8859-1");
response.setHeader("Content-Disposition", "attachment;filename=".concat(fileName));
out = response.getOutputStream();
byte[] buffer = new byte[512];// 缓冲区
int bytesToRead = -1;
// 通过循环将读入的Word文件的内容输出到浏览器中
while ((bytesToRead = fin.read(buffer)) != -1) {
out.write(buffer, 0, bytesToRead);
}
} finally {
if (fin != null) fin.close();
if (out != null) out.close();
if (file != null) file.delete();// 删除临时文件
}
}
private static File createDoc(Map<?, ?> dataMap, Template template, String name) {
File f = new File(name);
Template t = template;
try {
// 这个地方不能使用FileWriter因为需要指定编码类型否则生成的Word文档会因为有无法识别的编码而无法打开
Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");
t.process(dataMap, w);
w.close();
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
return f;
}
}
运行结果
4.拓展一些需求
1.对数据进行判空
如果传入的数据是个null,就会报错
还很好心的给了tip
If the failing expression is known to legally refer to something that’s sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??
翻译一下:
如果已知失败表达式合法地引用了某些有时为空或缺失的东西,则指定一个默认值,如myOptionalVar!当存在时使用<#if myOptionalVar??>,当缺少</#if>时使用<#else>。(这些只覆盖表达式的最后一步;要覆盖整个表达式,请使用括号:(myOptionalVar.foo)!myDefault (myOptionalVar.foo) ? ?
那咱就按照他的建议去改一下
先用(myOptionalVar.foo)!myDefault
【属性名】!【默认值】
原理是在用到值的地方去判空,如果为空的话,就取后面的默认值。
假设咱们这边是对grade.math进行判空,那就这么写
${grade.math!‘暂无成绩’}
查了一下官网文档,还有以下几种写法。
${grade.math?default(‘暂无成绩’)}
${grade.math???string(grade.math,‘暂无成绩’)}
咱们就用它提示的来写
导出一下看看
还真可以哎……再用if判断写一个
嗯……也没问题
2.对低于60分的成绩进行标红,高于90分的标绿
实现原理:在<w:rPr>这个标签中的<w:color>给他赋值,用if判断决定赋什么值
搞定!