SpringBoot 集成 FreeMarker 导出 Word 或 Excel 模板文件
思路解说
- word 模板文件(doc 或 docx 文件)另存为 xml 文件
- 将后缀 xml 改成 html;大部分文档会改成 ftl(FreeMarker 的后缀名),因为 word 文件另存为 xml 文件后,xml 文件中的代码很乱,后面的修改会很麻烦,因为我习惯用 VSCode 编辑器,安装 Beautify 插件后,可以自动格式化 html 代码,这样有利于后面的修改
- 将内容用
${param ! ''}
替换;例:姓名:月牙坠
-->姓名:${name ! ''}
- word 文件中的图片是 Base64 编码,我在这里封装了一个方法 imgUrl2Base64(图片地址转Base64编码)
top.yueyazhui.word_freemarker.util.ExportDocUtil.getImageBase64
;如果 imgUrl 中含有中文,将 imgUrl 以 "/" 切割成数组,含有中文的那部分用URLEncoder.encode("", UTF-8") 编码后,重新拼接 imgUrl;- 如果想要列表(表格)内容,在 html 文件中找到单个内容,在外层加
<#list favorites as favorite></#list>
- 封装导出 word 文件到客户端的方法
top.yueyazhui.word_freemarker.service.IExportDocService.exportDocToClient
- html 中引用的数据源是一个
Map<String, Object>
类型,所以传递数据的时候需要把Object
类型转成Map<String, Object>
类型- word 原文件
src/main/resources/attachment/info.doc
- 导出 Excel 与 导出 Word 基本一样;只需改变响应的内容类型:
response.setContentType("application/msword"); response.setContentType("application/msexcel");
- 导出 Excel 时,Excel 中不能有图片
- 在实际开发中,Word 可能会很复杂,需要用到多种 FreeMarker 语法,常用的 FreeMarker 语法
👀 注:
遇到列表时,考虑的情况
以 Word 模板列表条数为准(保持模板格式)
以数据的条数为准 (展示全部数据)
没有数据
数据条数小于 Word 模板列表条数
数据条数大于 Word 模板列表条数
FreeMarker 配置
#指定freemarker的模板路径和模板的后缀
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.suffix=.html
# 指定字符集
spring.freemarker.charset=utf-8
# 指定是否要启用缓存
spring.freemarker.cache=false
#指定是否要暴露请求和会话属性
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
FreeMarker 语法
特殊字符转义
<#noparse>
<#include "./common.ftl">
</#noparse>
插值
判断如果存在,就输出这个值
info.name??
判断如果不存在,就输出默认值
info.sex ! '未知'
info.name ! '' 为了防止 info.name 不存在报错
内置函数,调用区别于属性的访问,使用 ? 代替
字符串
html:对字符串进行HTML编码
cap_first:将字符串第一个字母大写
lower_case:将字符串转换成小写
trim:去掉字符串前后的空白字符
length: 获取字符长度
Sequences(序列)
size:获得序列中元素的数目
数字
int:取得数字的整数部分
简化
是否存在
info.name?if_exists
info.name?exists
info.name??
默认值
info.name?default("月牙坠")
info.name ! "月牙坠"
格式
日期格式
info.birth?string('yyyy-MM-dd')
三种不同的数字格式
info.num?string.number 20
info.num?string.currency $20.00
info.num?string.percent 20%
声明变量
<#assign mark = ture />
mark?string("yes","no") yes
条件
<#if info.score gte 60 && info.score lte 85>
及格
<#elseif info.score >= 85 && info.score <= 100>
优秀
<#else>
不及格
</#if>
gt:大于(greater than)
gte:大于等于(greater than or equal)
lt:小于(less than)
lte:小于等于(less than or equal)
eq:等于(equal)
neq:不等于
退出
<#break/>
switch
<#switch info.sex>
<#case 1>男<#break>
<#case 0>女<#break>
<#default>未知
</#switch>
循环
列表
<#list info.favorites as favorite>
获取列表大小:info.favorites?size
获取游标:favorite_index
判断是否有下一个元素:favorite_has_next
</#list>
数字
<#list 0..100 as i>
</#list>
前端(vue)
api
import request from '@/utils/request'
export function exportDoc() {
return request({
url: '/export/doc/',
method: 'get',
responseType: 'blob'
})
}
view
import { exportDoc } from '@/api/**'
exportDoc().then(res => {
var fileNameEncode = res.headers['content-disposition'].split('filename=')[1]
var fileName = decodeURIComponent(fileNameEncode)
const blob = new Blob([res.data], {
type: res.data.type
})
let link = document.createElement('a')
link.style.display = 'none'
let objectUrl = URL.createObjectURL(blob)
link.href = objectUrl
link.download = fileName
link.click()
URL.revokeObjectURL(objectUrl)
})
注:axios 的响应拦截器
// 二进制数据则直接返回
if (res.request.responseType === 'blob') {
return res
}