前言
FreeMarker是一款模板引擎,即一種基於模板和要改變的數據,並用來生成輸出文本的通用工具。
官方文檔:http://freemarker.foofun.cn
日常開發中,我們會有這樣的需求:
1、在頁面上填寫數據並導出word文檔、后台批量設置數據並導出Excel文檔(例如我們之前的博客記錄:html頁面轉PDF、圖片操作記錄)
2、寫一個代碼生成工具(例如我們之前的博客記錄:寫一個簡單的代碼生成器)
這種情況下我們可以用FreeMarker模板引擎來實現,本文記錄FreeMarker簡單操作。
代碼編寫
maven引入依賴
<dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency>
Word/Excel操作
Word操作
例如,要導出員工檔案,格式如下:
我們先按照格式要求,寫好word文檔,然后另存為xml,得到模板文件
用idea打開word.xml,格式化一下,然后根據FreeMarker的語法表達式讀取、設置值
封裝一個方法
//Word文檔操作 private static void createWord(){ System.out.println("開始Word文檔操作..."); //指定TemplateLoading模板根路徑 String rootPath = "E:\\Java\\test\\"; //模板文件 String templatePath = "word.xml"; //最終輸出文件路徑 String outFilePath = rootPath+"word_by_freemarker.docx"; //數據 Map<String, Object> data = new HashMap<>(); data.put("company","某某公司"); data.put("number","0001"); data.put("name","huanzi-qch"); data.put("phone","15600000000"); data.put("department","軟件開發部"); data.put("post","開發工程師"); ArrayList<Map<String, String>> works = new ArrayList<>(); Map<String, String> work1 = new HashMap<>(); work1.put("company","某某單位1"); work1.put("time","2018-01 - 2019-01"); work1.put("department","研發1部"); work1.put("post","開發工程師"); works.add(work1); Map<String, String> work2 = new HashMap<>(); work2.put("company","某某單位2"); work2.put("time","2019-01 - 2020-01"); work2.put("department","研發2部"); work2.put("post","開發工程師"); works.add(work2); data.put("works",works); try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){ //初始化,指定版本與pom文件相同 Configuration configuration = new Configuration(new Version("2.3.31")); configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString()); configuration.setDirectoryForTemplateLoading(new File(rootPath)); Template template = configuration.getTemplate(templatePath); //處理(數據) template.process(data, writer); } catch (IOException | TemplateException e) { e.printStackTrace(); } System.out.println("Word文檔操作結束!"); }
效果
Excel操作
Excel操作同理,也是先創建一個例子,另存為xml格式,再根據FreeMarker的語法表達式設置值
按照字段名,建Excel例子時直接把取值代碼寫進去,轉成xml后就可能省一點時間
同樣,idea打開后格式化一下,遍歷設置值
封裝一個方法
//Excel文檔操作 private static void createExcel(){ System.out.println("開始Excel文檔操作..."); //指定TemplateLoading模板根路徑 String rootPath = "E:\\Java\\test\\"; //模板文件 String templatePath = "excel.xml"; //最終輸出文件路徑 String outFilePath = rootPath+"excel_by_freemarker.xlsx"; //數據 Map<String, Object> data = new HashMap<>(); ArrayList<Map<String, String>> persons = new ArrayList<>(); Map<String, String> person1 = new HashMap<>(); person1.put("name","huanzi-qch1"); person1.put("phone","15600000000"); person1.put("company","某某單位1"); person1.put("time","2018-01 - 2019-01"); person1.put("department","研發1部"); person1.put("post","開發工程師"); persons.add(person1); Map<String, String> person2 = new HashMap<>(); person2.put("name","huanzi-qch2"); person2.put("phone","15600000000"); person2.put("company","某某單位2"); person2.put("time","2019-01 - 2020-01"); person2.put("department","研發2部"); person2.put("post","開發工程師"); persons.add(person2); data.put("persons",persons); try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){ //初始化,指定版本與pom文件相同 Configuration configuration = new Configuration(new Version("2.3.31")); configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString()); configuration.setDirectoryForTemplateLoading(new File(rootPath)); Template template = configuration.getTemplate(templatePath); //處理(數據) template.process(data, writer); } catch (IOException | TemplateException e) { e.printStackTrace(); } System.out.println("Excel文檔操作結束!"); }
效果
2021-10-29更新:如果需要調整文檔,直接打開我們的xml模板進行格式調整、如果添加圖片,在模板中插入圖片調整好格式,另存為xml格式,找到圖片的base64編碼,替換成動態參數即可(注:如果有使用模板語句關鍵字,要先刪掉再打開xml模板調整格式)
<#list listItem as item> </#list>
圖片轉base64
public static void main(String[] args) { System.out.println(imgFileToBase64("D:\\XFT User\\Pictures\\logo.png")); } /** * 圖片轉為base64 */ private static String imgFileToBase64(String imgFile){ byte[] data = null; try(InputStream in = new FileInputStream(imgFile);) { data = new byte[in.available()]; in.read(data); } catch (Exception e) { e.printStackTrace(); } return new BASE64Encoder().encode(data); }
代碼生成器
代碼生成器,先把我們之前寫好的JDBC連接數據庫工具類、字符串處理工具類、表結構信息實體類以及獲取表結構信息的方法先拿過來
/** * 程序自動設置 */ private static String tableName;//表名 private static String tableComment;//表注釋 /** * 數據連接相關,需要手動設置 */ private static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; private static final String DRIVER_CLASSNAME = "com.mysql.cj.jdbc.Driver"; /** * 字符串處理工具類 */ private static class StringUtil { /** * 數據庫類型->JAVA類型 * * @param dbType 數據庫類型 * @return JAVA類型 */ private static String typeMapping(String dbType) { String javaType; if ("int|integer".contains(dbType)) { javaType = "Integer"; } else if ("float|double|decimal|real".contains(dbType)) { javaType = "Double"; } else if ("date|time|datetime|timestamp".contains(dbType)) { javaType = "Date"; } else { javaType = "String"; } return javaType; } /** * 駝峰轉換為下划線 */ private static String underscoreName(String camelCaseName) { StringBuilder result = new StringBuilder(); if (camelCaseName != null && camelCaseName.length() > 0) { result.append(camelCaseName.substring(0, 1).toLowerCase()); for (int i = 1; i < camelCaseName.length(); i++) { char ch = camelCaseName.charAt(i); if (Character.isUpperCase(ch)) { result.append("_"); result.append(Character.toLowerCase(ch)); } else { result.append(ch); } } } return result.toString(); } /** * 首字母大寫 */ private static String captureName(String name) { char[] cs = name.toCharArray(); cs[0] -= 32; return String.valueOf(cs); } /** * 下划線轉換為駝峰 */ private static String camelCaseName(String underscoreName) { StringBuilder result = new StringBuilder(); if (underscoreName != null && underscoreName.length() > 0) { boolean flag = false; for (int i = 0; i < underscoreName.length(); i++) { char ch = underscoreName.charAt(i); if ("_".charAt(0) == ch) { flag = true; } else { if (flag) { result.append(Character.toUpperCase(ch)); flag = false; } else { result.append(ch); } } } } return result.toString(); } } /** * JDBC連接數據庫工具類 */ private static class DBConnectionUtil { static { // 1、加載驅動 try { Class.forName(DRIVER_CLASSNAME); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 返回一個Connection連接 */ static Connection getConnection() { Connection conn = null; // 2、連接數據庫 try { conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { e.printStackTrace(); } return conn; } /** * 關閉Connection,Statement連接 */ public static void close(Connection conn, Statement stmt) { try { conn.close(); stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } /** * 關閉Connection,Statement,ResultSet連接 */ public static void close(Connection conn, Statement stmt, ResultSet rs) { try { close(conn, stmt); rs.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * 表結構信息實體類 */ private static class TableInfo { private String columnName;//字段名 private String dataType;//字段類型 private String columnComment;//字段注釋 private String columnKey;//主鍵 private String extra;//主鍵類型 public String getColumnName() { return columnName; } public void setColumnName(String columnName) { this.columnName = columnName; } public String getDataType() { return dataType; } public void setDataType(String dataType) { this.dataType = dataType; } public String getColumnComment() { return columnComment; } public void setColumnComment(String columnComment) { this.columnComment = columnComment; } public String getColumnKey() { return columnKey; } public void setColumnKey(String columnKey) { this.columnKey = columnKey; } public String getExtra() { return extra; } public void setExtra(String extra) { this.extra = extra; } } /** * 獲取表結構信息 * 目前僅支持mysql */ private static List<TableInfo> getTableInfo() { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; ArrayList<TableInfo> list = new ArrayList<>(); try { conn = DBConnectionUtil.getConnection(); //表字段信息 String sql = "select column_name,data_type,column_comment,column_key,extra from information_schema.columns where table_schema = (select database()) and table_name=?"; ps = conn.prepareStatement(sql); ps.setString(1, tableName); rs = ps.executeQuery(); while (rs.next()) { TableInfo tableInfo = new TableInfo(); //列名,全部轉為小寫 tableInfo.setColumnName(rs.getString("column_name").toLowerCase()); //列類型 tableInfo.setDataType(rs.getString("data_type")); //列注釋 tableInfo.setColumnComment(rs.getString("column_comment")); //主鍵 tableInfo.setColumnKey(rs.getString("column_key")); //主鍵類型 tableInfo.setExtra(rs.getString("extra")); list.add(tableInfo); } //表注釋 sql = "select table_comment from information_schema.tables where table_schema = (select database()) and table_name=?"; ps = conn.prepareStatement(sql); ps.setString(1, tableName); rs = ps.executeQuery(); while (rs.next()) { //表注釋 tableComment = rs.getString("table_comment"); } } catch (SQLException e) { e.printStackTrace(); } finally { if(rs != null){ DBConnectionUtil.close(conn, ps, rs); } } return list; }
從原來的代碼生成器那里拿一個entity.tlf模板,小改動一下(主要是循環、if判斷那里不一樣)
package cn.huanzi.qch.baseadmin.sys.${entityToLowerCase}.pojo; import lombok.Data; import javax.persistence.*; import java.io.Serializable; import java.util.Date; /** * ${tableComment} 實體類 * * ${author} * ${date} */ @Entity @Table(name = "${tableName}") @Data public class ${entity} implements Serializable { <#list tableInfos as tableInfo> <#if tableInfo.columnKey == "PRI">@Id</#if> <#if tableInfo.extra == "auto_increment">@GeneratedValue(strategy= GenerationType.IDENTITY)</#if> private ${tableInfo.dataType} ${tableInfo.columnName};//${tableInfo.columnComment} </#list> }
封裝一個方法
//代碼生成 private static void autoGenerator(String tName){ System.out.println("開始代碼生成操作..."); tableName = tName; //指定TemplateLoading模板根路徑 String rootPath = "E:\\Java\\test\\"; //模板文件 String templatePath = "entity.tlf"; //最終輸出文件路徑 String outFilePath = rootPath+"entity.java"; //數據 Map<String, Object> data = new HashMap<>(); //駝峰標識映射后的表名 String captureName = StringUtil.captureName(StringUtil.camelCaseName(tableName)); //獲取表信息,並進行處理 List<TableInfo> tableInfoList = getTableInfo(); ArrayList<Map<String, String>> tableInfos = new ArrayList<>(); for (TableInfo info : tableInfoList) { HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("columnName", StringUtil.camelCaseName(info.getColumnName())); hashMap.put("dataType", StringUtil.typeMapping(info.getDataType())); hashMap.put("columnComment", info.getColumnComment()); hashMap.put("columnKey", info.getColumnKey()); hashMap.put("extra", info.getExtra()); tableInfos.add(hashMap); } data.put("entityToLowerCase",captureName.toLowerCase()); data.put("tableComment",tableComment); data.put("author","作者:Auto Generator By 'huanzi-qch'"); data.put("date","生成日期:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); data.put("tableName",tableName); data.put("entity",captureName); data.put("tableInfos",tableInfos); try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){ //初始化,指定版本與pom文件相同 Configuration configuration = new Configuration(new Version("2.3.31")); configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString()); configuration.setDirectoryForTemplateLoading(new File(rootPath)); Template template = configuration.getTemplate(templatePath); //處理(數據) template.process(data, writer); } catch (IOException | TemplateException e) { e.printStackTrace(); } System.out.println("代碼生成操作結束!"); }
效果
完整代碼

import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import freemarker.template.Version; import java.io.*; import java.nio.charset.StandardCharsets; import java.sql.*; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Date; public class Test { //Word文檔操作 private static void createWord(){ System.out.println("開始Word文檔操作..."); //指定TemplateLoading模板根路徑 String rootPath = "E:\\Java\\test\\"; //模板文件 String templatePath = "word.xml"; //最終輸出文件路徑 String outFilePath = rootPath+"word_by_freemarker.docx"; //數據 Map<String, Object> data = new HashMap<>(); data.put("company","某某公司"); data.put("number","0001"); data.put("name","huanzi-qch"); data.put("phone","15600000000"); data.put("department","軟件開發部"); data.put("post","開發工程師"); ArrayList<Map<String, String>> works = new ArrayList<>(); Map<String, String> work1 = new HashMap<>(); work1.put("company","某某單位1"); work1.put("time","2018-01 - 2019-01"); work1.put("department","研發1部"); work1.put("post","開發工程師"); works.add(work1); Map<String, String> work2 = new HashMap<>(); work2.put("company","某某單位2"); work2.put("time","2019-01 - 2020-01"); work2.put("department","研發2部"); work2.put("post","開發工程師"); works.add(work2); data.put("works",works); try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){ //初始化,指定版本與pom文件相同 Configuration configuration = new Configuration(new Version("2.3.31")); configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString()); configuration.setDirectoryForTemplateLoading(new File(rootPath)); Template template = configuration.getTemplate(templatePath); //處理(數據) template.process(data, writer); } catch (IOException | TemplateException e) { e.printStackTrace(); } System.out.println("Word文檔操作結束!"); } //Excel文檔操作 private static void createExcel(){ System.out.println("開始Excel文檔操作..."); //指定TemplateLoading模板根路徑 String rootPath = "E:\\Java\\test\\"; //模板文件 String templatePath = "excel.xml"; //最終輸出文件路徑 String outFilePath = rootPath+"excel_by_freemarker.xlsx"; //數據 Map<String, Object> data = new HashMap<>(); ArrayList<Map<String, String>> persons = new ArrayList<>(); Map<String, String> person1 = new HashMap<>(); person1.put("name","huanzi-qch1"); person1.put("phone","15600000000"); person1.put("company","某某單位1"); person1.put("time","2018-01 - 2019-01"); person1.put("department","研發1部"); person1.put("post","開發工程師"); persons.add(person1); Map<String, String> person2 = new HashMap<>(); person2.put("name","huanzi-qch2"); person2.put("phone","15600000000"); person2.put("company","某某單位2"); person2.put("time","2019-01 - 2020-01"); person2.put("department","研發2部"); person2.put("post","開發工程師"); persons.add(person2); data.put("persons",persons); try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){ //初始化,指定版本與pom文件相同 Configuration configuration = new Configuration(new Version("2.3.31")); configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString()); configuration.setDirectoryForTemplateLoading(new File(rootPath)); Template template = configuration.getTemplate(templatePath); //處理(數據) template.process(data, writer); } catch (IOException | TemplateException e) { e.printStackTrace(); } System.out.println("Excel文檔操作結束!"); } //代碼生成 private static void autoGenerator(String tName){ System.out.println("開始代碼生成操作..."); tableName = tName; //指定TemplateLoading模板根路徑 String rootPath = "E:\\Java\\test\\"; //模板文件 String templatePath = "entity.tlf"; //最終輸出文件路徑 String outFilePath = rootPath+"entity.java"; //數據 Map<String, Object> data = new HashMap<>(); //駝峰標識映射后的表名 String captureName = StringUtil.captureName(StringUtil.camelCaseName(tableName)); //獲取表信息,並進行處理 List<TableInfo> tableInfoList = getTableInfo(); ArrayList<Map<String, String>> tableInfos = new ArrayList<>(); for (TableInfo info : tableInfoList) { HashMap<String, String> hashMap = new HashMap<>(); hashMap.put("columnName", StringUtil.camelCaseName(info.getColumnName())); hashMap.put("dataType", StringUtil.typeMapping(info.getDataType())); hashMap.put("columnComment", info.getColumnComment()); hashMap.put("columnKey", info.getColumnKey()); hashMap.put("extra", info.getExtra()); tableInfos.add(hashMap); } data.put("entityToLowerCase",captureName.toLowerCase()); data.put("tableComment",tableComment); data.put("author","作者:Auto Generator By 'huanzi-qch'"); data.put("date","生成日期:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); data.put("tableName",tableName); data.put("entity",captureName); data.put("tableInfos",tableInfos); try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){ //初始化,指定版本與pom文件相同 Configuration configuration = new Configuration(new Version("2.3.31")); configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString()); configuration.setDirectoryForTemplateLoading(new File(rootPath)); Template template = configuration.getTemplate(templatePath); //處理(數據) template.process(data, writer); } catch (IOException | TemplateException e) { e.printStackTrace(); } System.out.println("代碼生成操作結束!"); } /** * 程序自動設置 */ private static String tableName;//表名 private static String tableComment;//表注釋 /** * 數據連接相關,需要手動設置 */ private static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; private static final String DRIVER_CLASSNAME = "com.mysql.cj.jdbc.Driver"; /** * 字符串處理工具類 */ private static class StringUtil { /** * 數據庫類型->JAVA類型 * * @param dbType 數據庫類型 * @return JAVA類型 */ private static String typeMapping(String dbType) { String javaType; if ("int|integer".contains(dbType)) { javaType = "Integer"; } else if ("float|double|decimal|real".contains(dbType)) { javaType = "Double"; } else if ("date|time|datetime|timestamp".contains(dbType)) { javaType = "Date"; } else { javaType = "String"; } return javaType; } /** * 駝峰轉換為下划線 */ private static String underscoreName(String camelCaseName) { StringBuilder result = new StringBuilder(); if (camelCaseName != null && camelCaseName.length() > 0) { result.append(camelCaseName.substring(0, 1).toLowerCase()); for (int i = 1; i < camelCaseName.length(); i++) { char ch = camelCaseName.charAt(i); if (Character.isUpperCase(ch)) { result.append("_"); result.append(Character.toLowerCase(ch)); } else { result.append(ch); } } } return result.toString(); } /** * 首字母大寫 */ private static String captureName(String name) { char[] cs = name.toCharArray(); cs[0] -= 32; return String.valueOf(cs); } /** * 下划線轉換為駝峰 */ private static String camelCaseName(String underscoreName) { StringBuilder result = new StringBuilder(); if (underscoreName != null && underscoreName.length() > 0) { boolean flag = false; for (int i = 0; i < underscoreName.length(); i++) { char ch = underscoreName.charAt(i); if ("_".charAt(0) == ch) { flag = true; } else { if (flag) { result.append(Character.toUpperCase(ch)); flag = false; } else { result.append(ch); } } } } return result.toString(); } } /** * JDBC連接數據庫工具類 */ private static class DBConnectionUtil { static { // 1、加載驅動 try { Class.forName(DRIVER_CLASSNAME); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 返回一個Connection連接 */ static Connection getConnection() { Connection conn = null; // 2、連接數據庫 try { conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { e.printStackTrace(); } return conn; } /** * 關閉Connection,Statement連接 */ public static void close(Connection conn, Statement stmt) { try { conn.close(); stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } /** * 關閉Connection,Statement,ResultSet連接 */ public static void close(Connection conn, Statement stmt, ResultSet rs) { try { close(conn, stmt); rs.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * 表結構信息實體類 */ private static class TableInfo { private String columnName;//字段名 private String dataType;//字段類型 private String columnComment;//字段注釋 private String columnKey;//主鍵 private String extra;//主鍵類型 public String getColumnName() { return columnName; } public void setColumnName(String columnName) { this.columnName = columnName; } public String getDataType() { return dataType; } public void setDataType(String dataType) { this.dataType = dataType; } public String getColumnComment() { return columnComment; } public void setColumnComment(String columnComment) { this.columnComment = columnComment; } public String getColumnKey() { return columnKey; } public void setColumnKey(String columnKey) { this.columnKey = columnKey; } public String getExtra() { return extra; } public void setExtra(String extra) { this.extra = extra; } } /** * 獲取表結構信息 * 目前僅支持mysql */ private static List<TableInfo> getTableInfo() { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; ArrayList<TableInfo> list = new ArrayList<>(); try { conn = DBConnectionUtil.getConnection(); //表字段信息 String sql = "select column_name,data_type,column_comment,column_key,extra from information_schema.columns where table_schema = (select database()) and table_name=?"; ps = conn.prepareStatement(sql); ps.setString(1, tableName); rs = ps.executeQuery(); while (rs.next()) { TableInfo tableInfo = new TableInfo(); //列名,全部轉為小寫 tableInfo.setColumnName(rs.getString("column_name").toLowerCase()); //列類型 tableInfo.setDataType(rs.getString("data_type")); //列注釋 tableInfo.setColumnComment(rs.getString("column_comment")); //主鍵 tableInfo.setColumnKey(rs.getString("column_key")); //主鍵類型 tableInfo.setExtra(rs.getString("extra")); list.add(tableInfo); } //表注釋 sql = "select table_comment from information_schema.tables where table_schema = (select database()) and table_name=?"; ps = conn.prepareStatement(sql); ps.setString(1, tableName); rs = ps.executeQuery(); while (rs.next()) { //表注釋 tableComment = rs.getString("table_comment"); } } catch (SQLException e) { e.printStackTrace(); } finally { if(rs != null){ DBConnectionUtil.close(conn, ps, rs); } } return list; } public static void main(String[] args) { // createWord(); // createExcel(); // autoGenerator("tb_user"); } }
后記
通過FreeMarker,按照固定格式,快速生成Word、Excel文檔,或者生成代碼,簡單高效。
生成文檔,數據來源可能是直接讀庫獲取,也有可能是要用戶在頁面上填寫,再傳入后台,這時候就可以將我們的模板文件,另存為html格式,小調整之后就可以展示給用戶,最大程度保證了用戶看到的文檔頁面跟生成、導出的文檔格式是一致的。