前端报表导出成word文档(含echarts图表)

前端报表导出成word文档(含echarts图表)

一、问题背景:

前端vue做的各种维度的报表,原来是通过前端整体截屏导出成PDF,但部分报表在遇到跨页时会被截断,客户体验极差。然后又考虑客户可能需要修改报表中的一些内容,因此需要导出成word文档解决跨页截断和满足修改报表内容的问题。前期解决方案预研时试过jacob、poi方案,但jacob只能用于windows平台(要引用一个dll文件),并且jacob和poi都存在样式方面的难题。后来通过其他渠道了解了freemarker,于是通过freemarker的把前端请求的报表数据填充到模板文件,生成word文档(导出功能由后端java实现)

二、效果图

首先上一张效果图,由于数据保密性,故前端页面的报表原样就不展示,导出的word文档的效果图和页面报表几乎一样
效果图

#三、功能点

  1. 文档标题
  2. 文档标题下方生成日期
  3. 文档总体情况概述
  4. 每个echarts图表的标题、图片、图注
  5. 水印
    #四、解决方案
    利用freemarker将前端传入的json格式数据填充入事先设计好的模板文件并生成word文档
    #五、实现流程
Created with Raphaël 2.2.0 开始 新建word文档 按照页面报表布局与样式设计文档模板 替换内容为占位符 另存为xml文件 xml模板占位符是否分离 占位符完整 将图表生成的base64编码手动替换成占位符 将文档保存到工程resource/freemarker/template目录 编写代码调用freemarker api生成word文档 访问swagger接口页面测试 下载,打开查看效果 yes no

#六、实现步骤
##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 {

    @Override
    public 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 {

    @Autowired
    private 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: 8080
  context-path: /zhuxl

##10. 创建swagger配置

@Configuration
@EnableSwagger2
public class SwaggerConfiguration {

    @Bean
    public 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文档下载并打开,效果图见文章顶部
#七、问题排查

  1. 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

  • 17
    点赞
  • 102
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 29
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽轩GM

您的鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值