PDF、EXCEL之动态模板编辑导出(ueditor+freemarker+itext+pdf.js+easypoi)

需求为:用户通过编辑模板,指定需要导出的字段,支持根据不同模板实现预览打印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老王于广州

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 21,783评论 1 92
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,367评论 6 343
  • 请财神,迎财神, 财神来了要开门, 财神进了门,全家长精神, 爸爸夸,妈妈笑,儿子高兴拍手跳, 财神爷来到了……
    吴知音阅读 357评论 0 0
  • 学生时代有很多学生时代的故事,成人了,就应该有很多成人的故事。可是如果学生时代的故事和成人的故事交织在一起,会是一...
    李一十八阅读 433评论 0 0