前端报表导出成word文档(含echarts图表)
前端报表导出成word文档(含echarts图表)
一、问题背景:
前端vue做的各种维度的报表,原来是通过前端整体截屏导出成PDF,但部分报表在遇到跨页时会被截断,客户体验极差。然后又考虑客户可能需要修改报表中的一些内容,因此需要导出成word文档解决跨页截断和满足修改报表内容的问题。前期解决方案预研时试过jacob、poi方案,但jacob只能用于windows平台(要引用一个dll文件),并且jacob和poi都存在样式方面的难题。后来通过其他渠道了解了freemarker,于是通过freemarker的把前端请求的报表数据填充到模板文件,生成word文档(导出功能由后端java实现)
二、效果图
首先上一张效果图,由于数据保密性,故前端页面的报表原样就不展示,导出的word文档的效果图和页面报表几乎一样
#三、功能点
- 文档标题
- 文档标题下方生成日期
- 文档总体情况概述
- 每个echarts图表的标题、图片、图注
- 水印
#四、解决方案
利用freemarker将前端传入的json格式数据填充入事先设计好的模板文件并生成word文档
#五、实现流程
#六、实现步骤
##1. 设计模板
按照前端报表展示样式,设计模板,并将模板中需要动态被参数填充的部分使用占位符代替,如标题使用${title},图表标题使用${title_1}、${title_2}、${title_3},图表总结词用${summary_1},${summary_2},${summary_3},以此类推.下图为使用占位符替换之后的word模板
##2. 另存模板为xml
上一步设计好模板并替换关键内容为占位符后,需要保存成xml模板文件,然后将xml模板文件中的图片base64编码替换成占位符,例如下面模板片段
<pkg:part pkg:name="/word/media/image16.png" pkg:contentType="image/png" pkg:compression="store"><pkg:binaryData>${base64_11}</pkg:binaryData></pkg:part><pkg:part pkg:name="/word/media/image11.png" pkg:contentType="image/png" pkg:compression="store"><pkg:binaryData>${base64_9_1}</pkg:binaryData></pkg:part><pkg:part pkg:name="/word/media/image9.png" pkg:contentType="image/png" pkg:compression="store"><pkg:binaryData>${base64_8_2}</pkg:binaryData></pkg:part><pkg:part pkg:name="/word/media/image10.png" pkg:contentType="image/png" pkg:compression="store"><pkg:binaryData>${base64_8_3}</pkg:binaryData></pkg:part><pkg:part pkg:name="/word/media/image8.png" pkg:contentType="image/png" pkg:compression="store"><pkg:binaryData>${base64_8_1}</pkg:binaryData></pkg:part>
##3. 新建maven工程
本人使用的开发工具是Idea 2018.1版本,创建maven项目并创建包名,结构如下:
export-doc
└─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─zhuxl
│ │ │ └─exportdoc
│ │ │ │
│ │ │ ├─component
│ │ │ │ └─handler
│ │ │ │
│ │ │ ├─configuration
│ │ │ │
│ │ │ ├─controller
│ │ │ │
│ │ │ ├─entity
│ │ │ │
│ │ │ ├─service
│ │ │ │ │
│ │ │ │ └─impl
│ │ │ │
│ │ │ └─util
│ │ │
│ │ └─resources
│ │
│ └─test
│ └─java
└─pom.xml
##4. 添加相关依赖
- 添加spring boot依赖
本demo项目基于spring boot框架,因此需要添加spring-boot-starter-web依赖,并且创建启动类Application.java
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>1.5.13.RELEASE</version><optional>true</optional>
</dependency>
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}
(exclude = {DataSourceAutoConfiguration.class})
参数表示不自动加载参数连接数据库,因为本demo无数据库连接,仅演示service里调用工具类方法导出word,不需要操作数据库,因此需要添加这个参数,否则启动会报连接数据库异常。
- 添加swagger依赖
本demo导出word报表请求参数为json格式,数据量非常大(因为有echarts报表base64编码),请求方式为POST,为了便于测试,因此集成swagger
<dependency><groupId>com.didispace</groupId><artifactId>spring-boot-starter-swagger</artifactId><version>1.4.1.RELEASE</version>
</dependency>
- 添加lombok依赖
demo中请求参数使用lombok注解@Data或@Getter,@Setter,可以不用写请求对象的getter和setter方法,在项目编译阶段会自动生成getter和setter方法。
<!-- LOMBOK begin -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.20</version>
</dependency>
<!-- LOMBOK end -->
- 添加fastjson依赖
demo可能会使用到JSONObject类来设置异常时接口返回的数据
<!-- FASTJSON begin -->
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.31</version>
</dependency>
<!-- FASTJSON end -->
- 添加freemarker依赖
该依赖为本次功能实现的核心,主要利用freemarker的api将请求数据构造的map和模板文件作为参数生成word文件,并返回File文件对象,最后使用response的输出流将文件返回
<!-- FREEMARKER begin -->
<dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId><version>2.3.28</version>
</dependency>
<!-- FREEMARKER end -->
##5. 创建接口请求参数对象类
使用java类来接收请求的json数据
@Data
@ApiModel(value = "贫困人群报表导出请求对象")
public class ReportExportWordRequest {@ApiModelProperty(value = "区域级别", name = "unitLevel")private Integer unitLevel;@ApiModelProperty(value = "区域编码", name = "unitCode")private String unitCode;@ApiModelProperty(value = "报表类型", name = "type", notes = "poverty:贫困人群报告;disable:残疾人群报告;poverty_disable:贫困且残疾人群报告")private String type;@ApiModelProperty(value = "报表标题", name = "title")private String title;@ApiModelProperty(value = "报告水印", name = "watermark")private String watermark;@ApiModelProperty(value = "报表生成日期", name = "date")private String date;@ApiModelProperty(value = "该区域报表描述第一段", name = "description1")private String description1;@ApiModelProperty(value = "该区域报表描述第二段", name = "description2")private String description2;@ApiModelProperty(value = "报表中每个图表的内容列表", name = "reports")private List<ReportContentRequest> reports;
}
@Data
@ApiModel("单个图表请求对象")
public class ReportContentRequest {@ApiModelProperty(value = "报表中排列序号", name = "serial")private Integer serial;@ApiModelProperty(value = "单个图表标题", name = "title")private String title;@ApiModelProperty(value = "单个图表base64编码值", name = "base64")private String base64;@ApiModelProperty(value = "单个图表内容总结", name = "summary")private String summary;@ApiModelProperty(value = "该标题下存在多个报表", name = "children")private List<ReportContentRequest> children;
}
6. 创建导出word工具类
该工具类是实现导出word功能的核心类,读取模板文件,格式化请求参数,填充模板生成word文档的功能都在此工具类完成
public class WordGeneratorUtils {private static Configuration configuration = null;private static Map<String, Template> allTemplates = null;private static class FreemarkerTemplate {public static final String POVERTY = "poverty";}static {configuration = new Configuration(Configuration.VERSION_2_3_28);configuration.setDefaultEncoding("utf-8");configuration.setClassForTemplateLoading(WordGeneratorUtils.class, "/freemarker/template");allTemplates = new HashMap();try {allTemplates.put(FreemarkerTemplate.POVERTY, configuration.getTemplate(FreemarkerTemplate.POVERTY + ".ftl"));} catch (IOException e) {e.printStackTrace();throw new RuntimeException(e);}}private WordGeneratorUtils() {throw new AssertionError();}public static File createDoc(Map<String, String> dataMap) {try {String name = dataMap.get("title") + dataMap.get("date") + ".doc";File f = new File(name);Template t = allTemplates.get(dataMap.get("template"));// 这个地方不能使用FileWriter因为需要指定编码类型否则生成的Word文档会因为有无法识别的编码而无法打开Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");t.process(dataMap, w);w.close();return f;} catch (Exception ex) {ex.printStackTrace();throw new RuntimeException("生成word文档失败");}}public static Map<String, String> parseToMap(ReportExportWordRequest request) {Map<String, String> datas = new HashMap(32);//主标题datas.put("title", request.getTitle());datas.put("date", request.getDate());datas.put("watermark", request.getWatermark());datas.put("description1", request.getDescription1());datas.put("description2", request.getDescription2());//遍历设置报表List<ReportContentRequest> contents = request.getReports();datas.put("template", request.getType());for (ReportContentRequest c : contents) {if (c.getChildren() == null || c.getChildren().size() == 0) {//无子报表datas.put("title_" + c.getSerial(), c.getTitle());datas.put("base64_" + c.getSerial(), c.getBase64());datas.put("summary_" + c.getSerial(), c.getSummary());} else {//有多个子报表datas.put("title_" + c.getSerial(), c.getTitle());for (ReportContentRequest subc : c.getChildren()) {datas.put("title_" + c.getSerial() + "_" + subc.getSerial(), subc.getTitle());datas.put("base64_" + c.getSerial() + "_" + subc.getSerial(), subc.getBase64());datas.put("summary_" + c.getSerial() + "_" + subc.getSerial(), subc.getSummary());}}}return datas;}
}
##7. 创建业务接口与实现类
ReportService
接口类
public interface ReportService {File exportWord(ReportExportWordRequest exportWordRequest);
}
ReportServiceImpl
实现类
@Service
public class ReportServiceImpl implements ReportService {@Overridepublic File exportWord(ReportExportWordRequest exportWordRequest) {//解析参数Map<String, String> datas = WordGeneratorUtils.parseToMap(exportWordRequest);//导出File word = WordGeneratorUtils.createDoc(datas);return word;}
}
8. 创建Controller类
@Slf4j
@RestController
@RequestMapping("/api/v1/report")
public class ReportController {@Autowiredprivate ReportService reportService;@ApiOperation(value = "贫困人群综合分析报告导出word文档", notes = "贫困人群综合分析报告导出word文档")@PostMapping("/poverty_export_word.ajax")public void povertyExportWord(HttpServletRequest request, HttpServletResponse response,@Valid @RequestBody ReportExportWordRequest exportWordRequest) {File file = reportService.exportWord(exportWordRequest);InputStream fin = null;OutputStream out = null;try {// 调用工具类WordGeneratorUtils的createDoc方法生成Word文档fin = new FileInputStream(file);response.setCharacterEncoding("utf-8");response.setContentType("application/msword");// 设置浏览器以下载的方式处理该文件// 设置文件名编码解决文件名乱码问题response.addHeader("Content-Disposition", "attachment;filename=" + new String(file.getName().getBytes(), "iso-8859-1"));out = response.getOutputStream();byte[] buffer = new byte[512];int bytesToRead = -1;// 通过循环将读入的Word文件的内容输出到浏览器中while ((bytesToRead = fin.read(buffer)) != -1) {out.write(buffer, 0, bytesToRead);}} catch (Exception e) {throw new RuntimeException("导出失败", e);} finally {try {if (fin != null) {fin.close();}if (out != null) {out.close();}if (file != null) {file.delete();}} catch (IOException e) {throw new RuntimeException("导出失败", e);}}}}
##9. 创建spring boot 启动类与yml配置
启动类在前面已经创建,此处只贴出application.yml基本配置
server:port: 8080context-path: /zhuxl
##10. 创建swagger配置
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {@Beanpublic Docket api() {ParameterBuilder parameterBuilder = new ParameterBuilder();parameterBuilder.name("Access-Token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();List<Parameter> parameters = new ArrayList<>();parameters.add(parameterBuilder.build());return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.any()).paths(PathSelectors.regex("/api/v1/.*")).build().globalOperationParameters(parameters).apiInfo(apiInfo());}
}
11. 运行,访问swagger页面测试
执行Applicaton类的main方法运行demo,访问swagger接口页面,在本demo中访问地址为:http://localhost:8080/zhuxl/swagger-ui.html
12. 构造参数测试获得报表word文件
构造json参数,点击try it out按钮,即可进行测试并将文件下载,由于请求参数中base64编码内容过于复杂,因此贴出的参数中图片base64编码省略
{"unitLevel":"4","unitCode":"513429100000","type":"poverty","title":"XXXX地区贫困人群总体情况报告","watermark":"张三13800138000","date":"2018年6月","description1":"报告正文开始的第一部分增加对该区域整体描述,描述中加入总关注人群数等信息","description2":"报告正文开始的第一部分增加对该区域整体描述,描述中加入总关注人群数等信息,报告正文开始的第一部分增加对该区域整体描述,描述中加入总关注人群数等信息,报告正文开始的第一部分增加对该区域整体描述,描述中加入总关注人群数等信息,报告正文开始的第一部分增加对该区域整体描述,描述中加入总关注人群数等信息,报告正文开始的第一部分增加对该区域整体描述,描述中加入总关注人群数等信息,报告正文开始的第一部分增加对该行政区域整体描述,描述中加入总关注人群数等信息,报告正文开始的第一部分增加对该行政区域整体描述,描述中加入总关注人群数等信息,报告正文开始的第一部分增加对该行政区域整体描述,描述中加入总关注人群数等信息","reports":[{"serial":1,"title":"一、贫困人口占比排名","base64":"xxxx","summary":"截止2018年6月,贫困人口占比最高的是XXX,占比达到60.8233%;占比最低的是XXXX,占比为11.6273%。","children":[]},{"serial":2,"title":"二、贫困人口关注等级分析","base64":"xxxxxxxx","summary":"截止2018年6月,总贫困人口中,一般关注等级0人,中度关注等级68156人,重点关注等级1人。三类人群分别占总人口的0%,34.2872%,0.0005%;占贫困总人口的0%,99.9985%,0.0014%。","children":[]},{"serial":3,"title":"三、致贫原因分析","base64":"xxxxx","summary":"截止2018年6月,贫困人口中,自身发展动力不足原因致贫的人数最多,占总贫困人口的37.0583%;其它原因致贫的人数最少,占总贫困人口的0.0161%。","children":[]},{"serial":4,"title":"四、贫困人群性别分析","base64":"xxxx","summary":"xxxxxxxxxxxxxxxxxxxxx","children":[]},{"serial":5,"title":"五、贫困人群年龄分析","base64":"xxxxx","summary":"","children":[]},{"serial":6,"title":"六、贫困人群学历分析","base64":"xxxxxx","summary":"","children":[]},{"serial":7,"title":"七、贫困人群民族分析","base64":"xxxxx","summary":"","children":[]},{"serial":8,"title":"八、贫困人群脱贫能力分析","base64":"","summary":"","children":[{"serial":1,"title":"1、文化程度分析","base64":"xxxxx","summary":"截止2018年6月,贫困人口中,中度关注等级中的文盲或半文盲,学龄前学历人数最多,占总贫困人口的30.7232%;重度关注等级中的大专及以上学历人数最少,占总贫困人口的0%。"},{"serial":2,"title":"2、劳动能力分析","base64":"xxxxxx","summary":"截止2018年6月,贫困人口中,中度关注等级中的丧失劳动力劳动力人数最多,占总贫困人口的51.1466%;重度关注等级中的技能劳动力劳动力人数最少,占总贫困人口的0%。"},{"serial":3,"title":"3、健康情况分析","base64":"xxxxx","summary":"截止2018年6月,贫困人口中,中度关注等级中的健康健康状况人数最多,占总贫困人口的96.5653%;重度关注等级中的残疾健康状况人数最少,占总贫困人口的0%。"}]},{"serial":9,"title":"九、资产和收入分析","base64":"","summary":"","children":[{"serial":1,"title":"1、家庭收入分析","base64":"xxxxx","summary":"截止2018年6月,贫困人口中,中度关注等级中的5k-10k收入人数最多,占总贫困人口的9.0365%;重度关注等级中的15k以上收入人数最少,占总贫困人口的0%"},{"serial":2,"title":"2、房产情况分析","base64":"xxxxx","summary":"截止2018年6月,贫困人口中,中度关注等级中的房屋面积50-100房产人数最多,占总贫困人口的18.8565%;重度关注等级中的房屋面积100平以上房产人数最少,占总贫困人口的0%"},{"serial":3,"title":"3、耕地林地情况分析","base64":"xxxxx","summary":"截止2018年6月,贫困人口中,中度关注等级中拥有耕地面积5.32亩以上亩的人数最多,占总贫困人口的7.7556%;重度关注等级中拥有耕地面积5.32亩以上亩的人数最少,占总贫困人口的0%"},{"serial":4,"title":"4、新农合、养老保险情况分析","base64":"xxxx","summary":"截止2018年6月,贫困人口中,中度关注等级中办理已参加新农合保险的人数最多,占总贫困人口的99.9956%;重度关注等级中办理已办理养老保险保险的人数最少,占总贫困人口的0%"}]},{"serial":10,"title":"十、预脱贫分析","base64":"xxxx","summary":"截止2018年6月,预脱贫人口中,2020年预脱贫的人数最多,占总贫困人口的2.4134%","children":[]},{"serial":11,"title":"十一、生活状况分析","base64":"xxxx","summary":"截止2018年6月,贫困人口家庭中,没有实现卫生厕所的贫困家庭数量占比最高,占比为80.7526%","children":[]}]
}
##13. 打开文件验证
将swagger接口页面Response Body请求返回的doc文档下载并打开,效果图见文章顶部
#七、问题排查
- doc模板设计保存成xml模板文件占位符分离,如${title_1}可能被分离成$、title_、1、}或者${title_、1}或者其他情况
方案一:手动修改xml中被分离的占位符,但缺点是如果模板需要做一点改动,保存的xml又需要手动修改,增加无谓的工作量
方案二:将整个占位符的样式设置成一样,但事实上同样存在被分离的情况
方案三:该方案可完美解决占位符分离情况,避免修改doc模板保存时重复修改占位符,点击查看详细方案
#八、git clone
传送门:去star
git clone https://github.com/v5zhu/export-doc.git
九、联系方式
QQ:2810010108
前端报表导出成word文档(含echarts图表)相关推荐
- 只涉及前端,将html页面导出成word文档
用docxtemplater插件,前端实现html导出word文档(可导出文字.表格.图片) 下面我用的是vue框架,用其他框架同理,只要这个插件能兼容就行 官方文档https://docxtempl ...
- PHP导出成word文档
文章转载:脚本之家 链接:https://www.jb51.net/article/157904.htm 作者:cywu https://mp.weixin.qq.com/s/OV33WGPgnjx7 ...
- CAD图纸如何免费导出成Word文档?
我们在日常操作CAD图纸的过程中经常会遇到格式转换的问题,比如将图纸转成Word?有哪些转换方法可以操作呢?需要再安装其他转换工具吗? 如果我们日常需要查看或编辑CAD图纸,那么一定已经安装过相关的C ...
- phpword 利用phpword将信息导出成word文档进行下载
前几天需要一个功能,就是把填写得信息(文字图片)导出到word,对于我这个小白来说无比的艰难,幸好有前辈得帮助.也希望能帮助更多的人~ 首先下载phpword https://download.cs ...
- 前端实现HTML导出为word文档
需求:将页面或者页面上所需要的部分导出为word文档 基本导出 修改样式 修改图片大小 修改导出文档名称 修改导出默认方式 准备工作: jquery FileSaver.js jquery.worde ...
- HTML导出生成Word文档
前言 在某某夜黑风高的一天即将下班的时候,老板召集公司大神们,进行了一个紧急会议,此会议主要目的是老板的客户提出了一些小需求, 有一个前端小 需求,需要将前端HTML导出为Word文档,因为没有做过此 ...
- js将html转为word文档,js将html导出到word文档(含echarts图表)
需求 在开发项目途中遇到了一个需求,就是将一个整个HTML界面导出到word文档,其中包含了echarts图表,经过一番折腾,终于完成了~~~(鸡肋),过程中用到了几个插件,总结了一下几个步骤,希望可 ...
- Java项目中利用Freemarker模板引擎导出--生成Word文档
应邀写的一篇文章:Java项目中利用Freemarker模板引擎导出--生成Word文档 资源下载:https://download.csdn.net/download/weixin_41367523 ...
- 用 Python 将微信热文转换成Word文档 | 神级操作
不得不说微信公众号已经成为了一个开放平台,每天数以万计的微信公众号文章在这产生,我们关注一个微信公众号每天便可以看到新的文章,我们同时也不知不觉的将好的文章分享到给朋友. 那么如何保存一个好的文章呢? ...
最新文章
- 终于把XGBoost总结写出来了!
- 开心庄园页面HTML素材,练习2:制作开心庄园页面.html
- Android实现侧滑抽屉菜单,android studio自带的抽屉侧滑菜单怎么设置点击事件?还头一回遇到,汗!...
- 一款jQuery立体感动态下拉导航菜单特效
- 深度学习(五十一)变分贝叶斯自编码器(上)
- 华为云PB级数据库GaussDB(for Redis)解析第二期:Redis消息队列Stream的应用探讨
- java 序列化 写入mysql_java 序列化到mysql数据库中
- 基于SSM框架的高校实验室管理系统PPT模板
- 【PostgreSQL-9.3.17】CentOS-6.7安装PostgreSQL-9.3.17
- 使用内部类或者外部类
- 从零基础入门Tensorflow2.0 ----一、3.4 实战深度神经网络(dropout)
- 【HTTP】Fiddler(一) - Fiddler简介和使用
- 堆栈宽度学习Stacked BLS的简单python代码实现
- C语言链表详解附实例
- indy-sdk tutorials数字身份认证(一)
- umts是移动还是联通_网络模式中的UMTS是什么意思?
- Python编写的com组件大全与解决对策
- inv如何用计算机计算,计算器INV是用那个键表示的
- 魔兽在副本里服务器维护了,魔兽世界11月19日维护服务器状态查询地址 6.2.3补丁上线更新一览...
- ADOBE AIR是什么?