我爱编程

PDF、EXCEL之动态模板编辑导出(ueditor+freem

2018-06-21  本文已影响0人  KICHUN

需求为:用户通过编辑模板,指定需要导出的字段,支持根据不同模板实现预览打印pdf及导出excel

思路:

  1. 模板编辑:前端使用富文本编辑器编辑所需的模板,通过拖拽或其他方式将不同字段的FreeMarker语法插入到指定位置
  2. 将编辑器的HTML模板保存至服务器
  3. 填充数据:使用FreeMarker根据HTML模板填充数据,生成包含数据的HTML字符串
  4. PDF预览及打印:根据填充数据后的HTML,使用itext生成pdf文件,前端使用pdf.js访问该文件进行预览及打印
  5. Excel导出:根据填充数据后的HTML,使用EasyPOI生成Excel文件,作为附件下载导出

模板编辑篇
这是最难做的,之前工作中有个用到编辑动态模板的,然后是两个前端高级工程师花了一个月直接改编辑器源码才实现功能。以我的水平,自然是分分钟搞不定的,只能先简单手动写个模板
使用百度ueditor


image.png

获取生成的HTML
坑1:重新从服务器拿回HTML特么会重新格式化,代码中&nbsp本来不应该存在,它自己加上去的
坑2:直接使用插入表格时,虽然在编辑器显示有表格边框,但实际html不带边框,需要自己编辑table border属性
坑3:不支持指定特殊属性,因为编辑器会格式化掉它不认识的东东

<p style="text-align: center;">
    <span style="text-decoration: underline; font-size: 24px;"><strong>模板标题</strong></span>
</p>
<p style="text-align: right;">
    <span style="font-size: 12px; text-decoration: none;">日期:${time}</span>
</p>
<p style="text-align: right;">
    <br/>
</p>
<table align="center" border="1" style="border-collapse:collapse;">
    <tbody>
        <tr class="firstRow">
            <td width="231" valign="top" align="center">
                <strong>ID</strong>
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
            </td>
            <td width="231" valign="top" align="center">
                <strong>父ID</strong>
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
            </td>
            <td width="231" valign="top" align="center">
                <strong>字典名</strong>
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
            </td>
            <td width="231" valign="top" align="center">
                <strong>备注</strong>
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
            </td>
        </tr>
        <tr class="ue-table-interlace-color-double">
            <td width="231" valign="top" align="center" style="word-break: break-all;">
                ${(data.id)!}
            </td>
            <td width="231" valign="top" align="center" style="word-break: break-all;">
                ${(data.parentId)!}
            </td>
            <td width="231" valign="top" align="center" style="word-break: break-all;">
                ${(data.name)!}
            </td>
            <td width="231" valign="top" align="center" style="word-break: break-all;">
                ${(data.comment)!}
            </td>
        </tr>
    </tbody>
</table>

数据填充篇
注意:模板来源是字符串而不是ftl文件,html是返回字符串而不是html文件,代码中ftl及html文件写出只是为了调试用
还是因为富文本编辑器原因,freemarker遍历list的语法是在这里处理的,这样限制了模板支持的格式,根源还是没有找到适合的编辑器,先这样处理

/**
     * 从模板获取填充数据后的html
     *
     * @author Qichang.Wang
     * @date 23:06 2018/6/19
     */
    private String getHtmlByTemplateId(String templateId, ParkingInfoRequestDto dto) throws Exception {
        ViewTemplate temp = null;
        if (StrUtil.isBlank(templateId)) {
            temp = viewTemplateService.selectById("1");
        } else {
            temp = viewTemplateService.selectById(templateId);
        }
                //这个是从数据库拿到的HTML
        String content = temp.getContent();

        //富文本编辑器无法处理ftl指令,手工处理
        int start = content.indexOf("</tr>") + "</tr>".length();
        int last = content.indexOf("</tbody>");
        StringBuffer sb = new StringBuffer(content);

        int indexTable = content.indexOf("<table")+"<table".length();
        //为表格添加sheetName属性,供easypoi导出excel时不报错
        sb.insert(indexTable," sheetName='currentSheet'");

        sb.insert(start, " <#list datas as data>");//<#if dicts?? && (dicts?size > 0) >
        String replace = StrUtil.replace(sb.toString(), "</tbody>", "</#list></tbody>");//</#if>

        File file = new File("D:\\Git\\traditional_web\\src\\main\\resources\\static\\temp.ftl");
        if (!FileUtil.exist(file)) {
            file.createNewFile();
        }

        ByteArrayInputStream is = new ByteArrayInputStream(replace.getBytes());
        FileOutputStream os = new FileOutputStream(file);
        byte[] bt = new byte[1024];
        int len;
        while ((len = is.read(bt)) != -1) {
            os.write(bt, 0, len);
        }
        is.close();
        os.close();

        //查询数据
        /*Page<ParkingInfo> page = parkingInfoService.getParkingInfoList(dto);
        List<ParkingInfo> rows = page.getRows();*/
        List<DataDict> dicts = dataDictService.selectList(new EntityWrapper<>(new DataDict()));
        TemplateOutDTO outDto = new TemplateOutDTO();
        outDto.setDatas(dicts);
        outDto.setTime( DateUtil.formatDateTime(new Date()));

        //配置从String载入模板内容
        Configuration conf = new Configuration(Configuration.VERSION_2_3_27);
        StringTemplateLoader loder = new StringTemplateLoader();
        loder.putTemplate("baseTemplate", replace);
        conf.setTemplateLoader(loder);

        //获取freemarker template
        Template baseTemplate = conf.getTemplate("baseTemplate");
        //使用freemarker填充数据,获取填充后的字符内容
        StringWriter writer = new StringWriter();

        baseTemplate.process(outDto,writer);

        String finalHtml = writer.toString();
        FileOutputStream fos = new FileOutputStream(new File("D:\\Git\\traditional_web\\src\\main\\resources\\static\\view\\temp.html"));
        fos.write(finalHtml.getBytes());
        fos.close();

        return finalHtml;

    }

填充数据后HTML文件

<p style="text-align: center;"><span style="text-decoration: underline; font-size: 24px;"><strong>模板标题</strong></span></p><p
        style="text-align: right;"><span style="font-size: 12px; text-decoration: none;">日期:2018-06-21 16:11:54</span></p><p
        style="text-align: right;"><br/></p>
<table sheetName='currentSheet' align="center" border="1" style="border-collapse:collapse;">
    <tbody>
    <tr class="firstRow">
        <td width="231" valign="top" align="center"><strong>ID</strong>
            &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</td>
        <td width="231" valign="top" align="center"><strong>父ID</strong>
            &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</td>
        <td width="231" valign="top" align="center"><strong>字典名</strong>
            &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</td>
        <td width="231" valign="top" align="center"><strong>备注</strong>
            &nbsp; &nbsp; &nbsp; &nbsp ; &nbsp; &nbsp;</td>
    </tr>
    <tr class="ue-table-interlace-color-double">
        <td width="231" valign="top" align="center" style="word-break: break-all;">1</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">ZD_VEHICLE_TYPE</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">车辆类型</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">顶级字典</td>
    </tr>
    ; &nbsp; &nbsp;</td></tr>
    <tr class="ue-table-interlace-color-double">
        <td width="231" valign="top" align="center" style="word-break: break-all;">2</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">ZD_CAUSE_OF_ACCIDENT</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">事故原因</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">顶级字典</td>
    </tr>
    ; &nbsp; &nbsp;</td></tr>
    <tr class="ue-table-interlace-color-double">
        <td width="231" valign="top" align="center" style="word-break: break-all;">3</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">ZD_STOCK_NUM</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">停放库号</td>
        <td width="231" valign="top" align="center" style="word-break: break-all;">顶级字典</td>
    </tr>
    </tbody>
</table>

PDF预览及打印
需要引入以下包

<!-- itext依赖 https://mvnrepository.com/artifact/com.itextpdf/itextpdf -->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13</version>
        </dependency>

        <!-- 亚洲中文依赖 https://mvnrepository.com/artifact/com.itextpdf/itext-asian -->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <!-- html2pdf https://mvnrepository.com/artifact/com.itextpdf.tool/xmlworker -->
        <dependency>
            <groupId>com.itextpdf.tool</groupId>
            <artifactId>xmlworker</artifactId>
            <version>5.5.13</version>
        </dependency>

HTML转PDF,非常简单,一句话搞定

    /**
     * 根据模板生成 pdf
     *
     * @author Qichang.Wang
     * @date 16:44 2018/6/19
     */
    @RequestMapping(value = "/getPdf", method = RequestMethod.GET)
    public void getPdf(HttpServletRequest request, HttpServletResponse response, String templateId, ParkingInfoRequestDto dto) {

        try {
            String content = this.getHtmlByTemplateId(templateId, dto);
            //将html内容转换为pdf
            // step 1
            Document document = new Document();
            // step 2
            PdfWriter pdfWriter = PdfWriter.getInstance(document, response.getOutputStream());
            // step 3
            document.open();
            // step 4
            XMLWorkerHelper.getInstance()
                    .parseXHtml(pdfWriter, document, new ByteArrayInputStream(content.getBytes()), CharsetUtil.charset("UTF-8"),
                            new AsianFontProvider());
            // step 5
            document.close();
        } catch (Exception e) {
            log.error("根据html获取pdf出错,{}", e.getMessage());
        }

    }

注意:要支持中文需要一个FontProvider

/**
 * xmlworker中文字体支持
 * Created by wangqichang on 2018/6/21.
 */
public class AsianFontProvider extends XMLWorkerFontProvider {

    @Override
    public Font getFont(final String fontname, String encoding, float size, final int style) {
        BaseFont bf = null;
        try {
            bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H",
                    BaseFont.NOT_EMBEDDED);
        } catch (Exception e) {
            Log.error("pdf:获取字体出错");
        }
        Font font = new Font(bf, size, style, BaseColor.BLACK);
        return font;
    }
}

前端使用pdf.js,使用方法请参考我的文章https://www.jianshu.com/p/36dbdeaee3ba
这里不再详述,直接上效果
有点渣渣,有空在调整

image.png

Excel导出
使用easypoi提供的转换工具,有性趣可以了解一下http://www.afterturn.cn/doc/easypoi.html
注意我在上面HTML中拼接的 table标签中的sheetName属性,该工具类目前需要通过该属性设置工作表名,否则无法转换。这个问题已经跟开发者沟通过,估计后期版本可以通过java代码参数传递sheetName

/**
     * 根据模板生成 Excel
     *
     * @author Qichang.Wang
     * @date 16:44 2018/6/19
     */
    @RequestMapping(value = "/getExcel", method = RequestMethod.GET)
    public void getExcel(HttpServletRequest request, HttpServletResponse response, String templateId, ParkingInfoRequestDto dto) {

        try {
            String content = this.getHtmlByTemplateId(templateId, dto);

            response.setContentType("application/force-download");//应用程序强制下载
            //设置响应头,对文件进行url编码
            String name = "xxx测试.xlsx";
            name = URLEncoder.encode(name, "UTF-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + name);

            //将html内容转换为Excel
            Workbook workbook = ExcelXorHtmlUtil.htmlToExcel(content, ExcelType.XSSF);
            workbook.write(response.getOutputStream());

        } catch (Exception e) {
            log.error("根据html获取Excel出错,{}", e.getMessage());
        }

    }

导出效果


image.png

模板 PDF Excel效果对比


image.png

目前这个功能仅在测试阶段,效果一般,细节有待调整,功能基本能达到需求
欢迎点赞评论及指出问题
18-06-21老王于广州

上一篇下一篇

猜你喜欢

热点阅读