这两天接到一个需求,要在系统中生成word版的需求规格说明书,领导给了个之前的样本给我,要求挺高,必须和给的样本基本一样。
基本样式主要有多级标题、动态图片、页眉页脚等,如下(内容部分因为隐私就不贴出来了):
当时的第一想法是用POI做,花了两个小时时间,果断放弃,POI 功能实现起来也挺简单,但是让我头疼的是样式,比如说行距、缩进、页眉页脚等。因为之前做过freemarker生成PDF、html之类的,所以选择freemarker。
主要步骤如下:
一、把word模板另存为xml格式
我这边是另存为 word2003 XML格式的,网上的说法是如果另存为wordXML的话,会存在2003的兼容性,这个我没去验证,但是我也试了下,如果另存为wordXML格式的话,两种生成模板的方式会略有不同,这里只介绍word2003XML这种方式,
二、生成模板后用notepade++之类的工具打开可以发现里面主要的数据结构都在里面,我这里有部分数据是固定的,所以只需要动态生成部分数据,如下:
这种多级标题需要生成的结构是:
这种数据结构应该都能看懂吧,一个递归搞定,图片问题的稍候再说,
另外再说一句,如果刚开始是另存为wordXML格式的模板,那么数据结构和这种完全不一样,比这个简单的多,但是在生成图片的时候会多操作一点,这个有兴趣的自己去尝试下吧(个人已经试过,比这种简单,但是因为在网上查看说有兼容性问题,所以比较担心,就没用)。
三、后台生成数据,这块不想详细说,说到底就是通过递归或者多层循环的方式做成需要的数据结构就行,简单的上点代码吧
递归的:
public static JSONArray treeRecursionDataList(List
两层循环的:
public static List
对于这两种方式: 个人建议如果只是简单的做数据格式,用两层循环就行,特别是你的数据或者层级比较多的情况下,用递归会使用栈内存,比较慢的,我的数据大概有3千条吧,层级稍多,用递归将近70000ms,但是两层循环1000ms左右。
我这里直接map做数据的,如果用实体也行,主要的几个字段无非是 id、pid、level、text、content、children、imgcode(用于展示图片),
最后生成的数据格式类似(比较懒,随便网上找了张图片):
四、后台数据做好了后,下面要做的就是前台渲染,那么如何做成前面所说的数据格式呢,我是用freemarker的宏递归
<#macro tree data>
<#list data as child>
<#if child?? && child.children?? && (child.children?size gt 0)>
。。。。。。。。。。。。。。
#if>
<@tree data=child.children />
<#else>
#if>
#if>
#list>
#macro>
<@tree data=reqList/>
主要结构就是这样,一张图片说明下
五、这样话只要一个生成doc的方法就可行了,直接上代码
/**
* 以下载的方式生成word,自定义路径
* @param dataMap
* @param out
*/
public void createDoc(Map dataMap,Writer out,String templateFile) {
// 设置模本装置方法和路径,FreeMarker支持多种模板装载方法。可以重servlet,classpath,数据库装载,
// ftl文件存放路径
try {
configure.setDirectoryForTemplateLoading(new File("d:/"));
Template t = null;
t = configure.getTemplate(templateFile);
t.setEncoding("utf-8");
t.process(dataMap, out);
} catch (IOException e) {
e.printStackTrace();
logger.error("读取文件出错!");
} catch (TemplateException e) {
e.printStackTrace();
logger.error("生成文件出错!");
}finally{
if (out != null){
try {
out.close();
} catch (IOException e) {
logger.error("流关闭出错!");
}
}
}
}
dataMap就是传到模板上的数据,我的是dataMap.put("reqList","做好的数据集合");templateFile是你的模板名称:如xxx.xml或者改成XXX.ftl也可以,上面这个方法是流下载的方式,如果生成到本地磁盘的话自己改改就行了,
主体的思路是这样的,这样的话应该可以生成word的了,部分代码就不上了,网上找找都有的
说下我遇到的问题吧:
图片生成系需要在具体的位置增加这段代码就行,
这个${child.base64code} 就是图片的base64编码,其中一下几点注意:
1和2出一定需要保持一致并且不能写死,之前我是写死的,生成文档是发现图片都是同一个图片,可以用文件的id替换,style中的width和height树形表示图片的高度和宽度,这块目前我没有做很好的处理,只是固定了一个大小,这样可能会出现部分过小或过大的图片产生变形,目前我测试了几张图片都是按照1.33的比例压缩的,即获取到图片的宽高,除以1.33即可,没想到比较好的方案,但是已经足够满足我的需求。
另外附上获取图片base64码的方法:
public Map getImageStr(String imgFile) {
// InputStream imgin = null;
// BufferedImage img = null;
ByteArrayOutputStream data = null;
String imgWidth = "";
String imgHeight = "";
String base64code = "";
try {
//获取此路径的连接
data = new ByteArrayOutputStream();
URL url = new URL(imgFile);
byte[] by = new byte[1024];
// 创建链接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5 * 1000);
InputStream is = conn.getInputStream();
// 将内容读取内存中
int len = -1;
while ((len = is.read(by)) != -1)
{
data.write(by, 0, len);
}
BASE64Encoder encoder = new BASE64Encoder();
base64code = encoder.encode(data.toByteArray());
// imgin = url.openStream();
// img = ImageIO.read(imgin);
// imgWidth = String.valueOf(img.getWidth());
// imgHeight = String.valueOf(img.getHeight());
} catch (FileNotFoundException e) {
logger.error("未发现该文件!");
} catch (IOException e) {
logger.error("读取文件出错!");
}finally {
try {
if (data != null){
data.close();
}
} catch (IOException e) {
logger.error("流关闭出错!");
}
}
Map result = new HashMap<>();
result.put("base64code",base64code);
// result.put("imgWidth",imgWidth);
// result.put("imgHeight",imgHeight);
return result;
}
2018.12.11.使用xml模板方式生成的word有一个问题就是手机端查看时,全是xml格式的代码,那是因为模板本身就是XML格式文件,freemarker使用的方式是用类型字符串替换的方式,替换掉XML里面的字符然后生成按相同格式生成文件,然后后缀名定为.doc而已。
由于XML文件的头部有这样的字符串,所以电脑上的office word读到这个信息后知道按转换xml里标签转换成word的格式。
但手机上的word软件则没有这个功能,所以就打开失败。
解决方法如下:
https://blog.csdn.net/FORLOVEHUAN/article/details/81452169
写的比较乱,但是整体思路就是这些,如果遇到问题可以留言讨论,或者各位有更好的解决方法的话,也请指正