1.2 任务要求

要求在前台能够下载word报告,报告中包含图片和表格。

在网上搜索了很多解决方案,大部分是利用freemark模板生成word,作者也试过这种方法,有几个缺点比较明显:

需要在word模板中写 ${name} 这种变量用于freemark替换,但是在word模板转换为 .xml 格式后 ${name} 可能不会连在一起,而是中间有xml中的代码,需要手动解决。

word模板转换为 .xml 后代码量很大,动辄上千行,比较容易出错,一旦出错就是文档打不开的结果。

如果生成的模板中带有图片,通常解决方案是在springboot后台将图片转换为base64格式然后替换掉 .xml 模板中的图片代码,但是这对图片格式以及base64图片数据中的空格等等都有很严格的要求,需要非常仔细,同时也非常麻烦。作者遇到最常见的就是图片代码被替换掉,但是图片显示不出来,需要一步一步的测试,非常麻烦,可能也与作者经验不足有关。

最后作者决定使用poi在后端生成word文档,然后前台进行下载的方法解决。

1.3 涉及到的知识点

  • 动态创建form表单并且向后端传参。
  • form表单如何传递对象中的数组。
  • 后端接收form表单传递的对象。
  • java中使用poi生成word文档。
  • word文档中创建图片。
  • word文档的下载。
  • 该文档下载接口的权限校验。
  • echarts中获取base64格式的图片。
  • 二、数据结构

    2.1 后端接收的数据结构

    使用VO类接收数据

    import lombok.Data;
    import java.util.List;
     * 接受数据的Vo类
    @Data
    public class TestVo {
         * key
        private String key;
         * value
        private String value;
         * chList
        private List<TestChildrenVo> chList;
    import lombok.Data;
     * 子类型Vo
    @Data
    public class TestChildrenVo {
         * 图片Base64
        private String img;
         * 分页大小
        private Integer pageSize;
         * 当前页
        private Integer pageNo;
    

    2.2 前端的数据结构

    let params = {
        key: '111',
        value: '222',
        chList: [{
            img: 'base64图片',
            pageSize: 10,
            pageNo: 1
            img: 'base64图片2',
            pageSize: 10,
            pageNo: 2
    

    三、前端数据发送

    首先为什么要用js创建form表单的方法请求,因为我们要下载word文件,如果使用ajax则下载下的是文件流,需要进行转换,作者曾使用ajax然后下载转blob类型下载word文件,但是这种方式下载后的word文件内容错误,无法打开,所以使用form表单post的方式下载。

    然后要把数据组装为2.2.2的那种数据结构

    3.1 base64图片的获取

    作者生成图片使用的是echarts,具体方法为:

    * 生成图片 getImageBase64() { return this.$refs.myChart.getImageBase64()

    其中myChart为echarts的实例

    3.2 组建参数

    这个就是postForm方法的第二个接收参数

    let params = {
        key: '111',
        value: '222',
        chList: [{
            img: 'base64图片',
            pageSize: 10,
            pageNo: 1
            img: 'base64图片2',
            pageSize: 10,
            pageNo: 2
    

    3.3 创建form表单

    * 模拟post请求 * url: 请求地址 * params:参数 postForm(url, params) { // 创建form元素 let temp_form = document.createElement('form') // 设置form属性 temp_form.action = url temp_form.target = '_self' temp_form.method = 'post' temp_form.style.display = 'none' // 处理需要传递的参数 for (let x in params) { if (params.hasOwnProperty(x)) { // 判断是否为数组,动态生成form数组实例 if (x === 'chList') { for (let i = 0; i < params[x].length; i++) { for (let y in params[x][i]) { let opt = document.createElement('textarea') opt.name = 'chList[' + i + '].' + y // 替换null和undefined为空字符串 opt.value = params[x][i][y] ? params[x][i][y] : '' temp_form.appendChild(opt) } else { let opt = document.createElement('textarea') opt.name = x // 替换null和undefined为空字符串 opt.value = params[x] ? params[x] : '' temp_form.appendChild(opt) document.body.appendChild(temp_form) // 提交表单 temp_form.submit() // 移除表单 document.body.removeChild(temp_form)

    以上代码组建的form大致为:

        <form action="xxx" method="post">
          <input name="key"/>
          <input name="value"/>
          <input name="chList[0].img"/>
          <input name="chList[0].pageSize"/>
          <input name="chList[0].pageNo"/>
          <input name="chList[1].img"/>
          <input name="chList[1].pageSize"/>
          <input name="chList[1].pageNo"/>
          <input name="chList[2].img"/>
          <input name="chList[2].pageSize"/>
          <input name="chList[2].pageNo"/>
        </form>
    

    关于form中如何提交数组的知识点:数组属性名称[索引].属性值

    其中数组中的属性和TestChildrenVo中的属性相对应

    注意,如果某一个属性为undefined,后台接收到的值为字符串的undefined,所以所有的参数不能为null或者undefined,可以为空字符串。

    四、后端处理

    POI版本

     		<!--数据导出excel-->
            <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi</artifactId>
                <version>3.17</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml</artifactId>
                <version>3.17</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml-schemas -->
            <dependency>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml-schemas</artifactId>
                <version>3.17</version>
            </dependency>
    

    4.1 数据接收

    TestVo详见2.2.1数据结构

    关于form提交数据的一些看法:本案例因为参数数据量较大,无法使用get请求,如果请求接口传递的参数不多并且数据量不是很大,则完全可以使用get请求传递参数,具体方式为:

    location.href = url + '?token=' + this.token + '&参数key=参数value'
    

    其中token用于该接口的权限验证,需要后台手动验证,如果不需要验证则可以忽略。

    后端数据接收方法:

    * word报告导出 @PostMapping(value = "/export-word") public void export(TestVo testVo, HttpServletResponse response) throws Exception { System.out.println(testVo.toString());

    4.2 创建word文件

      		XWPFDocument doc = new XWPFDocument();// 创建Word文件
            // 标题
            XWPFParagraph p = doc.createParagraph();// 新建段落
            p.setAlignment(ParagraphAlignment.CENTER);// 设置段落的对齐方式
            XWPFRun r = p.createRun();//创建标题
            r.setText("我是标题");
            r.setBold(true);//设置为粗体
            r.setColor("000000");//设置颜色
            r.setFontSize(21); //设置字体大小
            r.addCarriageReturn();//回车换行
            // 段落
            createParagraph(doc, "段落一");
            createParagraph(doc, "段落一");
            createParagraph(doc, "段落二");
            List<TestChildrenVo> imgs = testVo.getChList();
            for (TestChildrenVo img : imgs) {
                // 插入图片
                XWPFParagraph pImg = doc.createParagraph();
                pImg.setAlignment(ParagraphAlignment.CENTER);
                XWPFRun rImg = pImg.createRun();//创建标题
                rImg.addCarriageReturn();//回车换行
                String imgData = img.getImg().substring(img.getImg().indexOf(",") + 1);
                // 转为二进制
                byte[] bytes = new BASE64Decoder().decodeBuffer(imgData);
                // 向段落中插入图片
                rImg.addPicture(new ByteArrayInputStream(bytes), Document.PICTURE_TYPE_PNG, "123.png", Units.toEMU(400), Units.toEMU(180));
                // 表格
                XWPFTable table = doc.createTable(imgs.size() + 1, 2);
                //列宽自动分割
                CTTblWidth infoTableWidth = table.getCTTbl().addNewTblPr().addNewTblW();
                infoTableWidth.setType(STTblWidth.DXA);
                infoTableWidth.setW(BigInteger.valueOf(9072));
                setTableFonts(table.getRow(0).getCell(0), "当前页");
                setTableFonts(table.getRow(0).getCell(1), "分页大小");
                for (int i = 1; i <= imgs.size(); i++) {
                    setTableFonts(table.getRow(i).getCell(0), imgs.get(i - 1).getPageNo() + "");
                    setTableFonts(table.getRow(i).getCell(1), imgs.get(i - 1).getPageSize() + "");
    

    其中 String imgData = img.getImg().substring(img.getImg().indexOf(",") + 1);这段代码为了截取base64的数据后半段,data:image/png;base64,这些数据被截取掉,看下图:

    4.3 文件下载

          String fileName = "word导出测试_" + System.currentTimeMillis() + ".doc";
            String fileNameURL = URLEncoder.encode(fileName, "UTF-8");
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-disposition", "attachment;filename=" + fileNameURL + ";" + "filename*=utf-8''" + fileNameURL);
            response.setContentType("application/octet-stream");
            //刷新缓冲
            response.flushBuffer();
            OutputStream ouputStream = response.getOutputStream();
            doc.write(ouputStream);
            ouputStream.flush();
            ouputStream.close();
    

    4.4 word文件展示

    4.5 代码合并

    import lombok.extern.slf4j.Slf4j; import org.apache.poi.util.Units; import org.apache.poi.xwpf.usermodel.*; import org.openxmlformats.schemas.wordprocessingml.x2006.main.*; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import sun.misc.BASE64Decoder; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; import java.io.OutputStream; import java.math.BigInteger; import java.net.URLEncoder; import java.util.List; @Slf4j @RestController @RequestMapping("/test") public class WordExportTest { * word报告导出 @PostMapping(value = "/export-word") public void export(TestVo testVo, HttpServletResponse response) throws Exception { System.out.println(testVo.toString()); XWPFDocument doc = new XWPFDocument();// 创建Word文件 // 标题 XWPFParagraph p = doc.createParagraph();// 新建段落 p.setAlignment(ParagraphAlignment.CENTER);// 设置段落的对齐方式 XWPFRun r = p.createRun();//创建标题 r.setText("我是标题"); r.setBold(true);//设置为粗体 r.setColor("000000");//设置颜色 r.setFontSize(21); //设置字体大小 r.addCarriageReturn();//回车换行 // 段落 createParagraph(doc, "段落一"); createParagraph(doc, "段落一"); createParagraph(doc, "段落二"); List<TestChildrenVo> imgs = testVo.getChList(); for (TestChildrenVo img : imgs) { // 插入图片 XWPFParagraph pImg = doc.createParagraph(); pImg.setAlignment(ParagraphAlignment.CENTER); XWPFRun rImg = pImg.createRun();//创建标题 rImg.addCarriageReturn();//回车换行 String imgData = img.getImg().substring(img.getImg().indexOf(",") + 1); byte[] bytes = new BASE64Decoder().decodeBuffer(imgData); rImg.addPicture(new ByteArrayInputStream(bytes), Document.PICTURE_TYPE_PNG, "123.png", Units.toEMU(400), Units.toEMU(180)); // 表格 XWPFTable table = doc.createTable(imgs.size() + 1, 2); //列宽自动分割 CTTblWidth infoTableWidth = table.getCTTbl().addNewTblPr().addNewTblW(); infoTableWidth.setType(STTblWidth.DXA); infoTableWidth.setW(BigInteger.valueOf(9072)); setTableFonts(table.getRow(0).getCell(0), "当前页"); setTableFonts(table.getRow(0).getCell(1), "分页大小"); for (int i = 1; i <= imgs.size(); i++) { setTableFonts(table.getRow(i).getCell(0), imgs.get(i - 1).getPageNo() + ""); setTableFonts(table.getRow(i).getCell(1), imgs.get(i - 1).getPageSize() + ""); String fileName = "word导出测试_" + System.currentTimeMillis() + ".doc"; String fileNameURL = URLEncoder.encode(fileName, "UTF-8"); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileNameURL + ";" + "filename*=utf-8''" + fileNameURL); response.setContentType("application/octet-stream"); //刷新缓冲 response.flushBuffer(); OutputStream ouputStream = response.getOutputStream(); doc.write(ouputStream); ouputStream.flush(); ouputStream.close(); * 创建段落 * @param text private void createParagraph(XWPFDocument doc, String text) { XWPFParagraph paragraph = doc.createParagraph();// 新建段落 //paragraph.setAlignment(ParagraphAlignment.LEFT);// 设置段落的对齐方式 paragraph.setFontAlignment(1);//字体对齐方式:1左对齐 2居中3右对齐 XWPFRun run = paragraph.createRun();//创建标题 run.setText(text); run.setColor("000000");//设置颜色 run.setFontSize(14); //设置字体大小 run.addCarriageReturn();//回车换行 * 设置表格中字体 * @param cell * @param cellText private static void setTableFonts(XWPFTableCell cell, String cellText) { CTP ctp = CTP.Factory.newInstance(); XWPFParagraph p = new XWPFParagraph(ctp, cell); p.setAlignment(ParagraphAlignment.CENTER); XWPFRun run = p.createRun(); run.setFontSize(14); run.setText(cellText); CTRPr rpr = run.getCTR().isSetRPr() ? run.getCTR().getRPr() : run.getCTR().addNewRPr(); CTFonts fonts = rpr.isSetRFonts() ? rpr.getRFonts() : rpr.addNewRFonts(); fonts.setAscii("仿宋"); fonts.setEastAsia("仿宋"); fonts.setHAnsi("仿宋"); cell.setParagraph(p);

    五、接口权限校验

    SpringBoot后端权限校验使用的是jwt,需要在请求头中添加token;但是form表单无法添加请求头,则token要添加到post的参数中,然后在后端接收token进行手动校验用户是否登陆或者其他权限。如果该接口不需要进行认证,则可以直接放行。

    分类:
    后端
    标签: