|
|
@@ -0,0 +1,354 @@
|
|
|
+package com.hotent.util.wordexcelutils;/**
|
|
|
+ * @program: cbjs-mvue-master
|
|
|
+ * @description:
|
|
|
+ * @author: zhao yue yue
|
|
|
+ * @create: 2025-11-26 16:47
|
|
|
+ */
|
|
|
+
|
|
|
+import org.apache.poi.xwpf.usermodel.*;
|
|
|
+
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ *@author: zhao yue yue
|
|
|
+ *@create: 2025-11-26 16:47
|
|
|
+ */
|
|
|
+public class SmartTemplateWriter {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 智能写入:保证数据写入 + 保持样式
|
|
|
+ */
|
|
|
+ public static boolean writeToTemplate(XWPFDocument templateDoc,
|
|
|
+ Map<String, String> dataMap) {
|
|
|
+ if (templateDoc == null || dataMap == null) return false;
|
|
|
+
|
|
|
+ boolean allSuccess = true;
|
|
|
+
|
|
|
+ // 遍历所有占位符
|
|
|
+ for (Map.Entry<String, String> entry : dataMap.entrySet()) {
|
|
|
+ String placeholder = entry.getKey(); // 如 ${name}
|
|
|
+ String value = entry.getValue();
|
|
|
+
|
|
|
+ boolean success = replacePlaceholderSmart(templateDoc, placeholder, value);
|
|
|
+ if (!success) {
|
|
|
+ System.err.println("警告: 占位符 " + placeholder + " 替换失败");
|
|
|
+ allSuccess = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return allSuccess;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 智能替换占位符
|
|
|
+ */
|
|
|
+ private static boolean replacePlaceholderSmart(XWPFDocument doc, String placeholder, String value) {
|
|
|
+ boolean found = false;
|
|
|
+
|
|
|
+ // 1. 首先在段落中查找
|
|
|
+ for (XWPFParagraph paragraph : doc.getParagraphs()) {
|
|
|
+ if (replaceInParagraphSmart(paragraph, placeholder, value)) {
|
|
|
+ found = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 在表格中查找
|
|
|
+ for (XWPFTable table : doc.getTables()) {
|
|
|
+ for (XWPFTableRow row : table.getRows()) {
|
|
|
+ for (XWPFTableCell cell : row.getTableCells()) {
|
|
|
+ for (XWPFParagraph paragraph : cell.getParagraphs()) {
|
|
|
+ if (replaceInParagraphSmart(paragraph, placeholder, value)) {
|
|
|
+ found = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 如果还没找到,使用强制写入(最后手段)
|
|
|
+ if (!found) {
|
|
|
+ found = forceWriteToFirstAvailable(doc, placeholder, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ return found;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在段落中智能替换
|
|
|
+ */
|
|
|
+ private static boolean replaceInParagraphSmart(XWPFParagraph paragraph, String placeholder, String value) {
|
|
|
+ List<XWPFRun> runs = paragraph.getRuns();
|
|
|
+ boolean foundInThisParagraph = false;
|
|
|
+
|
|
|
+ // 方法1:在单个Run中直接替换(最佳情况)
|
|
|
+ for (XWPFRun run : runs) {
|
|
|
+ String text = run.getText(0);
|
|
|
+ if (text != null && text.contains(placeholder)) {
|
|
|
+ replaceInSingleRun(run, placeholder, value);
|
|
|
+ foundInThisParagraph = true;
|
|
|
+ break; // 找到一个就退出,避免重复替换
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方法2:跨Run替换
|
|
|
+ if (!foundInThisParagraph) {
|
|
|
+ String fullText = paragraph.getText();
|
|
|
+ if (fullText != null && fullText.contains(placeholder)) {
|
|
|
+ foundInThisParagraph = replaceAcrossRuns(paragraph, placeholder, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return foundInThisParagraph;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在单个Run中替换(保持格式)
|
|
|
+ */
|
|
|
+ private static void replaceInSingleRun(XWPFRun run, String placeholder, String value) {
|
|
|
+ try {
|
|
|
+ String text = run.getText(0);
|
|
|
+ if (text != null && text.contains(placeholder)) {
|
|
|
+ // 保存原始格式
|
|
|
+ RunStyle originalStyle = saveRunStyle(run);
|
|
|
+
|
|
|
+ // 执行替换
|
|
|
+ String newText = text.replace(placeholder, value);
|
|
|
+ run.setText(newText, 0);
|
|
|
+
|
|
|
+ // 恢复格式
|
|
|
+ restoreRunStyle(run, originalStyle);
|
|
|
+
|
|
|
+ System.out.println("成功在单个Run中替换: " + placeholder);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ System.err.println("单个Run替换失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 跨Run替换(复杂但必要)
|
|
|
+ */
|
|
|
+ private static boolean replaceAcrossRuns(XWPFParagraph paragraph, String placeholder, String value) {
|
|
|
+ try {
|
|
|
+ List<XWPFRun> runs = paragraph.getRuns();
|
|
|
+ if (runs.isEmpty()) return false;
|
|
|
+
|
|
|
+ // 构建完整文本并记录Run信息
|
|
|
+ StringBuilder fullText = new StringBuilder();
|
|
|
+ List<RunInfo> runInfos = new ArrayList<>();
|
|
|
+
|
|
|
+ int currentPos = 0;
|
|
|
+ for (XWPFRun run : runs) {
|
|
|
+ String text = run.getText(0);
|
|
|
+ if (text == null) text = "";
|
|
|
+
|
|
|
+ RunInfo info = new RunInfo();
|
|
|
+ info.run = run;
|
|
|
+ info.text = text;
|
|
|
+ info.startPos = currentPos;
|
|
|
+ info.endPos = currentPos + text.length();
|
|
|
+ runInfos.add(info);
|
|
|
+
|
|
|
+ fullText.append(text);
|
|
|
+ currentPos += text.length();
|
|
|
+ }
|
|
|
+
|
|
|
+ String completeText = fullText.toString();
|
|
|
+ int startIndex = completeText.indexOf(placeholder);
|
|
|
+
|
|
|
+ if (startIndex == -1) return false;
|
|
|
+
|
|
|
+ int endIndex = startIndex + placeholder.length();
|
|
|
+
|
|
|
+ // 找到受影响的Run
|
|
|
+ List<RunInfo> affectedRuns = new ArrayList<>();
|
|
|
+ for (RunInfo info : runInfos) {
|
|
|
+ if (info.endPos > startIndex && info.startPos < endIndex) {
|
|
|
+ affectedRuns.add(info);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (affectedRuns.isEmpty()) return false;
|
|
|
+
|
|
|
+ // 执行跨Run替换
|
|
|
+ return executeCrossRunReplacement(paragraph, runInfos, affectedRuns,
|
|
|
+ startIndex, endIndex, placeholder, value);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ System.err.println("跨Run替换失败: " + e.getMessage());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行跨Run替换
|
|
|
+ */
|
|
|
+ private static boolean executeCrossRunReplacement(XWPFParagraph paragraph,
|
|
|
+ List<RunInfo> allRunInfos,
|
|
|
+ List<RunInfo> affectedRuns,
|
|
|
+ int startIndex, int endIndex,
|
|
|
+ String placeholder, String value) {
|
|
|
+ try {
|
|
|
+ // 保存第一个受影响Run的格式作为基准
|
|
|
+ XWPFRun baseRun = affectedRuns.get(0).run;
|
|
|
+ RunStyle baseStyle = saveRunStyle(baseRun);
|
|
|
+
|
|
|
+ // 构建新文本
|
|
|
+ String fullText = getParagraphFullText(paragraph);
|
|
|
+ String newFullText = fullText.replace(placeholder, value);
|
|
|
+
|
|
|
+ // 清除段落内容但保留段落属性
|
|
|
+ clearParagraphContent(paragraph);
|
|
|
+
|
|
|
+ // 重新创建Run并设置文本
|
|
|
+ XWPFRun newRun = paragraph.createRun();
|
|
|
+ newRun.setText(newFullText);
|
|
|
+
|
|
|
+ // 应用基准格式
|
|
|
+ restoreRunStyle(newRun, baseStyle);
|
|
|
+
|
|
|
+ System.out.println("跨Run替换成功: " + placeholder);
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ System.err.println("执行跨Run替换失败: " + e.getMessage());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 强制写入:最后手段
|
|
|
+ */
|
|
|
+ private static boolean forceWriteToFirstAvailable(XWPFDocument doc, String placeholder, String value) {
|
|
|
+ System.out.println("使用强制写入: " + placeholder);
|
|
|
+
|
|
|
+ // 找到第一个可用的段落
|
|
|
+ for (XWPFParagraph paragraph : doc.getParagraphs()) {
|
|
|
+ if (isParagraphWritable(paragraph)) {
|
|
|
+ return forceWriteToParagraph(paragraph, placeholder, value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果段落都不可写,创建新段落
|
|
|
+ return createNewParagraphWithData(doc, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 强制写入到段落
|
|
|
+ */
|
|
|
+ private static boolean forceWriteToParagraph(XWPFParagraph paragraph, String placeholder, String value) {
|
|
|
+ try {
|
|
|
+ // 保存第一个Run的格式
|
|
|
+ RunStyle style = null;
|
|
|
+ if (!paragraph.getRuns().isEmpty()) {
|
|
|
+ style = saveRunStyle(paragraph.getRuns().get(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除内容
|
|
|
+ clearParagraphContent(paragraph);
|
|
|
+
|
|
|
+ // 写入新内容
|
|
|
+ XWPFRun newRun = paragraph.createRun();
|
|
|
+ newRun.setText(value);
|
|
|
+
|
|
|
+ // 恢复格式
|
|
|
+ if (style != null) {
|
|
|
+ restoreRunStyle(newRun, style);
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ System.err.println("强制写入失败: " + e.getMessage());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建新段落并写入数据
|
|
|
+ */
|
|
|
+ private static boolean createNewParagraphWithData(XWPFDocument doc, String value) {
|
|
|
+ try {
|
|
|
+ XWPFParagraph newParagraph = doc.createParagraph();
|
|
|
+ XWPFRun newRun = newParagraph.createRun();
|
|
|
+ newRun.setText(value);
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ System.err.println("创建新段落失败: " + e.getMessage());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 工具方法
|
|
|
+ */
|
|
|
+ private static String getParagraphFullText(XWPFParagraph paragraph) {
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (XWPFRun run : paragraph.getRuns()) {
|
|
|
+ String text = run.getText(0);
|
|
|
+ if (text != null) {
|
|
|
+ sb.append(text);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void clearParagraphContent(XWPFParagraph paragraph) {
|
|
|
+ for (XWPFRun run : paragraph.getRuns()) {
|
|
|
+ run.setText("", 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isParagraphWritable(XWPFParagraph paragraph) {
|
|
|
+ return paragraph != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存和恢复Run样式的方法(使用之前的安全版本)
|
|
|
+ private static RunStyle saveRunStyle(XWPFRun run) {
|
|
|
+ RunStyle style = new RunStyle();
|
|
|
+ if (run == null) return style;
|
|
|
+
|
|
|
+ style.bold = run.isBold();
|
|
|
+ style.italic = run.isItalic();
|
|
|
+ style.fontFamily = run.getFontFamily();
|
|
|
+ style.fontSize = run.getFontSize();
|
|
|
+ style.color = run.getColor();
|
|
|
+ style.underline = run.getUnderline();
|
|
|
+ return style;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void restoreRunStyle(XWPFRun run, RunStyle style) {
|
|
|
+ if (run == null || style == null) return;
|
|
|
+
|
|
|
+ run.setBold(style.bold);
|
|
|
+ run.setItalic(style.italic);
|
|
|
+ if (style.fontFamily != null) run.setFontFamily(style.fontFamily);
|
|
|
+ if (style.fontSize != null) run.setFontSize(style.fontSize);
|
|
|
+ if (style.color != null) run.setColor(style.color);
|
|
|
+ if (style.underline != null) run.setUnderline(style.underline);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 辅助类
|
|
|
+ */
|
|
|
+ static class RunStyle {
|
|
|
+ boolean bold;
|
|
|
+ boolean italic;
|
|
|
+ String fontFamily;
|
|
|
+ Integer fontSize;
|
|
|
+ String color;
|
|
|
+ UnderlinePatterns underline;
|
|
|
+ }
|
|
|
+
|
|
|
+ static class RunInfo {
|
|
|
+ XWPFRun run;
|
|
|
+ String text;
|
|
|
+ int startPos;
|
|
|
+ int endPos;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+}
|