关于富文本html通过java生成并导出word 项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。现要求将合同生成word并在线下载。

项目场景:

项目背景:由于前端用其他平台的缘故不支持直接引用三方库本地开发,仅可利用平台在线的方式。
现要求将合同生成word并在线下载。


问题描述

思路有很多,后端赋值并通过poi生成一篇word文档,通过前端处理富文本模板并将数据代入传至后台生成word文档

第一种思路:

1.制作word合同模板。
2.将值传递替换进模板占位符。
3.将替换完成模板导出。

这里试过这一种思路:弊端是代码量很多,在替换时会遍历每一行并将占位符进行替换,在此过程中有可能会将格式搞错,例如把下划线去掉了,改了字体颜色,多加部分空格下划线。

提供工具类
package app.modules.common.util;
import cn.hutool.core.lang.Assert;
import lombok.Data;
import lombok.Setter;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
public class DynWordUtils {
 private final Logger logger = LoggerFactory.getLogger(DynWordUtils.class);
 /**
 * 被list替换的段落 被替换的都是oldParagraph
 */
 private XWPFParagraph oldParagraph;
 /**
 * 参数
 */
 private Map paramMap;
 /**
 * 当前元素的位置
 */
 int n = 0;
 /**
 * 判断当前是否是遍历的表格
 */
 boolean isTable = false;
 /**
 * 模板对象
 */
 XWPFDocument templateDoc;
 /**
 * 默认字体的大小
 */
 final int DEFAULT_FONT_SIZE = 10;
 /**
 * 重复模式的占位符所在的行索引
 */
 private int currentRowIndex;
 /**
 * 入口
 *
 * @param paramMap 模板中使用的参数
 * @param templatePaht 模板全路径
 * @param outPath 生成的文件存放的本地全路径
 */
 public static void process(Map paramMap, String templatePaht, String outPath, HttpServletRequest request, HttpServletResponse response) {
 DynWordUtils dynWordUtils = new DynWordUtils();
 dynWordUtils.setParamMap(paramMap);
 dynWordUtils.createWord(templatePaht, outPath,request,response);
 }
 private void setParamMap(Map paramMap) {
 this.paramMap = paramMap;
 }
 /**
 * 生成动态的word
 * @param templatePath
 * @param outPath
 */
 public void createWord(String templatePath, String outPath,HttpServletRequest request, HttpServletResponse response) {
 File inFile = new File(templatePath);
 try (OutputStream out = response.getOutputStream()) {
 templateDoc = new XWPFDocument(OPCPackage.open(inFile));
 parseTemplateWord();
 //写入流中
// inputStream =new FileInputStream(inFile);
 //获取响应流
 // 写入响应流中
// byte[] buffer = new byte[inputStream.available()];
// // 流中写入的是字节
// int bytesRead;
// // 读入流写出流
// while ((bytesRead = inputStream.read(buffer)) != -1) {
// outputStream.write(buffer, 0, bytesRead);
// }
 //设置响应头和contentType
 response.setHeader("Content-Disposition", "attachment;filename=exported_document.docx");
 response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
 templateDoc.write(out);
 out.flush();
 // 结束关流,不关流会导致文件损坏
 out.close();
 } catch (Exception e) {
 StackTraceElement[] stackTrace = e.getStackTrace();
 String className = stackTrace[0].getClassName();
 String methodName = stackTrace[0].getMethodName();
 int lineNumber = stackTrace[0].getLineNumber();
 logger.error("错误:第:{}行, 类名:{}, 方法名:{}", lineNumber, className, methodName);
 throw new RuntimeException(e.getCause().getMessage());
 }
 }
 /**
 * 解析word模板
 */
 public void parseTemplateWord() throws Exception {
 List elements = templateDoc.getBodyElements();
 for (; n < elements.size(); n++) {
 IBodyElement element = elements.get(n);
 // 普通段落
 if (element instanceof XWPFParagraph) {
 XWPFParagraph paragraph = (XWPFParagraph) element;
 oldParagraph = paragraph;
 if (paragraph.getParagraphText().isEmpty()) {
 continue;
 }
 delParagraph(paragraph);
 }
 else if (element instanceof XWPFTable) {
 // 表格
 isTable = true;
 XWPFTable table = (XWPFTable) element;
 delTable(table, paramMap);
 isTable = false;
 }
 }
 }
 /**
 * 处理段落
 */
 private void delParagraph(XWPFParagraph paragraph) throws Exception {
 List runs = oldParagraph.getRuns();
 StringBuilder sb = new StringBuilder();
 for (XWPFRun run : runs) {
 String text = run.getText(0);
 if (text == null) {
 continue;
 }
 sb.append(text);
 run.setText("", 0);
 }
 Placeholder(paragraph, runs, sb);
 }
 /**
 * 匹配传入信息集合与模板
 *
 * @param placeholder 模板需要替换的区域()
 * @param paramMap 传入信息集合
 * @return 模板需要替换区域信息集合对应值
 */
 public void changeValue(XWPFRun currRun, String placeholder, Map paramMap) throws Exception {
 String placeholderValue = placeholder;
 if (paramMap == null || paramMap.isEmpty()) {
 return;
 }
// if (str!=null&str.contains(PoiWordUtils.underlineRowText)) {
// currRun.setUnderline(UnderlinePatterns.SINGLE);
// placeholderValue = placeholder.replace(PoiWordUtils.underlineRowText,"");
// }
 Map stringBooleanSet= new HashMap();
 Set textSets = paramMap.entrySet();
 for (Map.Entry textSet : textSets) {
 //匹配模板与替换值 格式${key}
 String mapKey = textSet.getKey();
 String docKey = PoiWordUtils.getDocKey(mapKey);
 if (placeholderValue.indexOf(docKey) != -1) {
 Object obj = textSet.getValue();
 // 需要添加一个list
 if (obj instanceof List) {
 placeholderValue = delDynList(placeholder, (List) obj);
 } else {
 placeholderValue = placeholderValue.replaceAll(PoiWordUtils.getPlaceholderReg(mapKey), String.valueOf(obj));
 }
 }
 }
 currRun.setText(placeholderValue, 0);
 }
 /**
 * 处理的动态的段落(参数为list)
 *
 * @param placeholder 段落占位符
 * @param obj
 * @return
 */
 private String delDynList(String placeholder, List obj) {
 String placeholderValue = placeholder;
 List dataList = obj;
 Collections.reverse(dataList);
 for (int i = 0, size = dataList.size(); i < size; i++) {
 Object text = dataList.get(i);
 // 占位符的那行, 不用重新创建新的行
 if (i == 0) {
 placeholderValue = String.valueOf(text);
 } else {
 XWPFParagraph paragraph = createParagraph(String.valueOf(text));
 if (paragraph != null) {
 oldParagraph = paragraph;
 }
 // 增加段落后doc文档会的element的size会随着增加(在当前行的上面添加),回退并解析新增的行(因为可能新增的带有占位符,这里为了支持图片和表格)
 if (!isTable) {
 n--;
 }
 }
 }
 return placeholderValue;
 }
 /**
 * 创建段落 
 *
 * @param texts
 */
 public XWPFParagraph createParagraph(String... texts) {
 // 使用游标创建一个新行
 XmlCursor cursor = oldParagraph.getCTP().newCursor();
 XWPFParagraph newPar = templateDoc.insertNewParagraph(cursor);
 // 设置段落样式
 newPar.getCTP().setPPr(oldParagraph.getCTP().getPPr());
 copyParagraph(oldParagraph, newPar, texts);
 return newPar;
 }
 /**
 * 处理表格(遍历)
 *
 * @param table 表格
 * @param paramMap 需要替换的信息集合
 */
 public void delTable(XWPFTable table, Map paramMap) throws Exception {
 List rows = table.getRows();
 for (int i = 0, size = rows.size(); i < size; i++) {
 XWPFTableRow row = rows.get(i);
 currentRowIndex = i;
 // 如果是动态添加行 直接处理后返回
 if (delAndJudgeRow(table, paramMap, row)) {
 return;
 }
 }
 }
 /**
 * 判断并且是否是动态行,并且处理表格占位符
 * @param table 表格对象
 * @param paramMap 参数map
 * @param row 当前行
 * @return
 * @throws Exception
 */
 private boolean delAndJudgeRow(XWPFTable table, Map paramMap, XWPFTableRow row) throws Exception {
 // 当前行是动态行标志
 if (PoiWordUtils.isAddRow(row)) {
 List xwpfTableRows = addAndGetRows(table, row, paramMap);
 // 回溯添加的行,这里是试图处理动态添加的图片
 for (XWPFTableRow tbRow : xwpfTableRows) {
 delAndJudgeRow(table, paramMap, tbRow);
 }
 return true;
 }
 // 如果是重复添加的行
 if (PoiWordUtils.isAddRowRepeat(row)) {
 List xwpfTableRows = addAndGetRepeatRows(table, row, paramMap);
 // 回溯添加的行,这里是试图处理动态添加的图片
 for (XWPFTableRow tbRow : xwpfTableRows) {
 delAndJudgeRow(table, paramMap, tbRow);
 }
 return true;
 }
 // 当前行非动态行标签
 List cells = row.getTableCells();
 for (XWPFTableCell cell : cells) {
 //判断单元格是否需要替换
 if (PoiWordUtils.checkText(cell.getText())) {
 List paragraphs = cell.getParagraphs();
 for (XWPFParagraph paragraph : paragraphs) {
 List runs = paragraph.getRuns();
 StringBuilder sb = new StringBuilder();
 for (XWPFRun run : runs) {
 sb.append(run.toString());
 run.setText("", 0);
 }
 Placeholder(paragraph, runs, sb);
 }
 }
 }
 return false;
 }
 /**
 * 处理占位符
 * @param runs 当前段的runs
 * @param sb 当前段的内容
 * @throws Exception
 */
 private void Placeholder(XWPFParagraph currentPar, List runs, StringBuilder sb) throws Exception {
 if (runs.size() > 0) {
 String text = sb.toString();
 List newRuns = currentPar.getRuns();
 XWPFRun currRun = runs.get(0);
 if (PoiWordUtils.isPicture(text)) {
 // 该段落是图片占位符
 } else {
 changeValue(currRun, text, paramMap);
 }
 }
 }
 /**
 * 添加行 标签行不是新创建的
 * flagRow flagRow 表有标签的行
 */
 private List addAndGetRows(XWPFTable table, XWPFTableRow flagRow, Map paramMap) throws Exception {
 List flagRowCells = flagRow.getTableCells();
 XWPFTableCell flagCell = flagRowCells.get(0);
 String text = flagCell.getText();
 List dataList = (List) PoiWordUtils.getValueByPlaceholder(paramMap, text);
 // 新添加的行
 List newRows = new ArrayList(dataList.size());
 if (dataList == null || dataList.size() 0 ? sourcePar.getRuns().get(0) : null;
 for (int i = 0, len = texts.length; i < len; i++) {
 String text = arr[i];
 XWPFRun run = targetPar.createRun();
 run.setText(text);
 run.setFontFamily(xwpfRun.getFontFamily());
 int fontSize = xwpfRun.getFontSize();
 run.setFontSize((fontSize == -1) ? DEFAULT_FONT_SIZE : fontSize);
 run.setBold(xwpfRun.isBold());
 run.setItalic(xwpfRun.isItalic());
 }
 }
 }
}
package app.modules.common.util;
import org.apache.poi.xwpf.usermodel.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class PoiWordUtils {
 /**
 * 占位符第一个字符
 */
 public static final String PREFIX_FIRST = "$";
 /**
 * 占位符第二个字符
 */
 public static final String PREFIX_SECOND = "{";
 /**
 * 占位符的前缀
 */
 public static final String PLACEHOLDER_PREFIX = PREFIX_FIRST + PREFIX_SECOND;
 /**
 * 占位符后缀
 */
 public static final String PLACEHOLDER_END = "}";
 /**
 * 下划线格式标记
 * */
 public static final String underlineRowText = "underline:";
 /**
 * 表格中需要动态添加行的独特标记
 */
 public static final String addRowText = "tbAddRow:";
 public static final String addRowRepeatText = "tbAddRowRepeat:";
 /**
 * 表格中占位符的开头 ${tbAddRow: 例如${tbAddRow:tb1}
 */
 public static final String addRowFlag = PLACEHOLDER_PREFIX + addRowText;
 /**
 * 表格中占位符的开头 ${tbAddRowRepeat: 例如 ${tbAddRowRepeat:0,2,0,1} 第0行到第2行,第0列到第1列 为模板样式
 */
 public static final String addRowRepeatFlag = PLACEHOLDER_PREFIX + addRowRepeatText;
 /**
 * 重复矩阵的分隔符 比如:${tbAddRowRepeat:0,2,0,1} 分隔符为 ,
 */
 public static final String tbRepeatMatrixSeparator = ",";
 /**
 * 占位符的后缀
 */
 public static final String PLACEHOLDER_SUFFIX = "}";
 /**
 * 图片占位符的前缀
 */
 public static final String PICTURE_PREFIX = PLACEHOLDER_PREFIX + "image:";
 private static final Logger log = LoggerFactory.getLogger(PoiWordUtils.class);
 /**
 * 判断当前行是不是标志表格中需要添加行
 *
 * @param row
 * @return
 */
 public static boolean isAddRow(XWPFTableRow row) {
 return isDynRow(row, addRowFlag);
 }
 /**
 * 添加重复模板动态行(以多行为模板)
 * @param row
 * @return
 */
 public static boolean isAddRowRepeat(XWPFTableRow row) {
 return isDynRow(row, addRowRepeatFlag);
 }
 private static boolean isDynRow(XWPFTableRow row, String dynFlag) {
 if (row == null) {
 return false;
 }
 List tableCells = row.getTableCells();
 if (tableCells != null) {
 XWPFTableCell cell = tableCells.get(0);
 if (cell != null) {
 String text = cell.getText();
 if (text != null & text.startsWith(dynFlag)) {
 return true;
 }
 }
 }
 return false;
 }
 /**
 * 从参数map中获取占位符对应的值
 *
 * @param paramMap
 * @param key
 * @return
 */
 public static Object getValueByPlaceholder(Map paramMap, String key) {
 if (paramMap != null) {
 if (key != null) {
 return paramMap.get(getKeyFromPlaceholder(key));
 }
 }
 return null;
 }
 /**
 * 后去占位符的重复行列矩阵
 * @param key 占位符
 * @return {0,2,0,1}
 */
 public static String getTbRepeatMatrix(String key) {
 String $1 = key.replaceAll("\\" + PREFIX_FIRST + "\\" + PREFIX_SECOND + addRowRepeatText + "(.*)" + "\\" + PLACEHOLDER_SUFFIX, "$1");
 return $1;
 }
 /**
 * 从占位符中获取key
 *
 * @return
 */
 public static String getKeyFromPlaceholder(String placeholder) {
 return Optional.ofNullable(placeholder).map(p -> p.replaceAll("[\\$\\{\\}]", "")).get();
 }
 /**
 * 复制列的样式,并且设置值
 * @param sourceCell
 * @param targetCell
 * @param text
 */
 public static void copyCellAndSetValue(XWPFTableCell sourceCell, XWPFTableCell targetCell, String text) {
 //段落属性
 List sourceCellParagraphs = sourceCell.getParagraphs();
 if (sourceCellParagraphs == null || sourceCellParagraphs.size() 0) {
 // 如果当前cell中有run
 List runs = targetPar.getRuns();
 Optional.ofNullable(runs).ifPresent(rs -> rs.stream().forEach(r -> r.setText("", 0)));
 if (runs != null & runs.size() > 0) {
 runs.get(0).setText(text, 0);
 } else {
 XWPFRun cellR = targetPar.createRun();
 cellR.setText(text, 0);
 // 设置列的样式位模板的样式
 targetCell.getCTTc().setTcPr(sourceCell.getCTTc().getTcPr());
 }
 setTypeface(sourcePar, targetPar);
 } else {
 targetCell.setText(text);
 }
 }
 /**
 * 复制字体
 */
 private static void setTypeface(XWPFParagraph sourcePar, XWPFParagraph targetPar) {
 XWPFRun sourceRun = sourcePar.getRuns().get(0);
 String fontFamily = sourceRun.getFontFamily();
 //int fontSize = sourceRun.getFontSize();
 String color = sourceRun.getColor();
// String fontName = sourceRun.getFontName();
 boolean bold = sourceRun.isBold();
 boolean italic = sourceRun.isItalic();
 int kerning = sourceRun.getKerning();
// String style = sourcePar.getStyle();
 UnderlinePatterns underline = sourceRun.getUnderline();
 XWPFRun targetRun = targetPar.getRuns().get(0);
 targetRun.setFontFamily(fontFamily);
// targetRun.setFontSize(fontSize == -1 ? 10 : fontSize);
 targetRun.setBold(bold);
 targetRun.setColor(color);
 log.info("存在颜色变动=====》");
 targetRun.setItalic(italic);
 targetRun.setKerning(kerning);
 targetRun.setUnderline(underline);
 //targetRun.setFontSize(fontSize);
 }
 /**
 * 判断文本中时候包含$
 * @param text 文本
 * @return 包含返回true,不包含返回false
 */
 public static boolean checkText(String text){
 boolean check = false;
 if(text.indexOf(PLACEHOLDER_PREFIX)!= -1){
 check = true;
 }
 return check;
 }
 /**
 * 获得占位符替换的正则表达式
 * @return
 */
 public static String getPlaceholderReg(String text) {
 return "\\" + PREFIX_FIRST + "\\" + PREFIX_SECOND + text + "\\" + PLACEHOLDER_SUFFIX;
 }
 public static String getDocKey(String mapKey) {
 return PLACEHOLDER_PREFIX + mapKey + PLACEHOLDER_SUFFIX;
 }
 /**
 * 判断当前占位符是不是一个图片占位符
 * @param text
 * @return
 */
 public static boolean isPicture(String text) {
 return text.startsWith(PICTURE_PREFIX);
 }
 /**
 * 删除一行的列
 * @param row
 */
 public static void removeCells(XWPFTableRow row) {
 int size = row.getTableCells().size();
 try {
 for (int i = 0; i < size; i++) {
 row.removeCell(i);
 }
 } catch (Exception e) {
 }
 }
}
  1. 调用部分
// 处理导出合同
 @PostMapping("/getContractWord")
 public void getContractWord(@RequestBody Map jsonObject, HttpServletRequest request, HttpServletResponse response) {
 Map map =new HashMap();
 map.put("customerName","高密海绵厂");
 map.put("salesName","烟台顺达聚氨酯有限责任公司");
 map.put("contractNo","123456");
 log.info("============>{}",map.toString());
 DynWordUtils.process(map,"D:\\bbb.docx","D:\\aaa.doc",request,response);
 }

第二种思路

1.前端准备富文本html利用原生css处理格式。
2.前端利用es6模板语法将html中的关键字段值替换掉
3.传递后端生成word 并写入流中

1.html很多 这里只取片段 包含普通格式以及表格处理

es6只需要代码块中存在声明占位符部分,模板会自动填充

let data = this.value
 const date = new Date(data.createdTime); // 获取当前日期
 const year = date.getFullYear(); // 获取年份
 const month = date.getMonth() + 1; // 获取月份,需要加1,因为月份是从0开始计数的
 const day = date.getDate(); // 获取日期
 const formattedDate = `${year}年${month}月${day}日`;
 let html1 =
 `
 
 
 
 工业品买卖合同
 
 
 
 
 
 
 供方:${data.orgName}
 
 
 合同编号:${data.formNo}
 
 
 
 
 
 签订方式:传真、扫描件
 
 
 
 
 需方:${data.CustomerName}
 
 
 签订时间:${formattedDate}
 
 
 
 
 第一条 产品规格、数量、价款
 
 
 
 
 
 
 
 
 
 序号
 
 
 
 
 产品名称
 
 
 
 
 牌号
 
 
 
 
 规格
 
 
 
 
 数量(吨)
 
 
 
 
 单价(元/吨)
 
 
 
 
 总金额(元)
 
 
 
 
 备注
 
 
 `
 let data1 = this.value.ws_contractentry
 data1.forEach((item,index)=>{
 html1+=` 
 
 ${index+1}
 
 
 
 ${item.groupName}
 
 
 
 
 ${item.mNameIdText}
 
 
 
 
 ${item.model}
 
 
 
 ${item.num}
 
 
 
 ${item.rPrice}
 
 
 
 
 ${item.sum}
 
 
 
 
 ${item.remark}
 
 
 `;
 })
 // 发送数据到服务器
 fetch(`${config.apiHost}/common/createWord`, {
 method: 'POST',
 headers: {
 'Content-Type': 'application/json'
 },
 body: JSON.stringify({ html: html1+html2 })
 })
 .then(response => response.blob())
 .then(blob => {
 // 创建一个下载链接并触发下载
 const url = window.URL.createObjectURL(blob);
 const a = document.createElement('a');
 a.href = url;
 a.download = 'document.docx'; // 文件名
 document.body.appendChild(a);
 a.click();
 a.remove();
 })
 .catch(error => console.error('Error:', error));
java部分 html转成word导出
@PostMapping("/createWord")
 public void generateWord(@RequestBody Map html, HttpServletRequest request, HttpServletResponse response) throws IOException {
 // 读取富文本数据
 String htmlContent = html.get("html");
 // 必须要html标识的标签 否则可能会解析不出来 
 htmlContent = ""+htmlContent+"";
 ByteArrayInputStream byteArrayInputStream = null;
 POIFSFileSystem poifsFileSystem = null;
 ServletOutputStream servletOutputStream = null;
 try {
 byte b[] = htmlContent.getBytes("GBK"); //这里是必须要设置编码的,不然导出中文就会乱码。
 byteArrayInputStream = new ByteArrayInputStream(b);//将字节数组包装到流中
 poifsFileSystem = new POIFSFileSystem();
 DirectoryEntry directory = poifsFileSystem.getRoot();
 directory.createDocument("WordDocument", byteArrayInputStream);
 //输出文件
 request.setCharacterEncoding("utf-8");
 response.setContentType("application/msword");//导出word格式
 response.addHeader("Content-Disposition", "attachment;filename=aaaa.doc");
 servletOutputStream = response.getOutputStream();
 poifsFileSystem.writeFilesystem(servletOutputStream);
 byteArrayInputStream.close();
 servletOutputStream.close();
 poifsFileSystem.close();
 } catch (Exception e) {
 throw e;
 }
 }

第二种思路的弊端是poifsFileSystem不支持二次修改,例如修改页边距 修改字体颜色 调整文件内容,我尝试了一下poifsFileSystem转HWPFDdocument对象没找到合适的解决方法 所以暂定不能二次修改。


最后效果

作者:getExpectObject()原文地址:https://blog.csdn.net/KingOdin/article/details/141391600

%s 个评论

要回复文章请先登录注册