diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/BookmarkExec.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/BookmarkExec.java index 2636b99..d22fd16 100644 --- a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/BookmarkExec.java +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/BookmarkExec.java @@ -3,8 +3,13 @@ package com.wmyun.farmwork.word.core; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.file.FileNameUtil; +import com.google.common.collect.Maps; import com.wmyun.farmwork.word.core.model.BookmarkInfo; import com.wmyun.farmwork.word.core.model.BookmarkReplaceDataModel; +import com.wmyun.farmwork.word.core.model.TableBookmarkInfo; +import com.wmyun.farmwork.word.core.model.ext.ListExData; +import com.wmyun.farmwork.word.core.model.ext.TableExData; +import com.wmyun.farmwork.word.core.model.ext.TextExData; import lombok.extern.slf4j.Slf4j; import org.apache.poi.xwpf.usermodel.*; import org.apache.xmlbeans.XmlCursor; @@ -38,15 +43,31 @@ public class BookmarkExec { private static final String ATTR_BOOKMARK_ID = "w:id"; private static final String ATTR_BOOKMARK_NAME = "w:name"; + private static final String TABLE_ROW_BOOKMARK_START = "w:colFirst"; + private static final String TABLE_ROW_BOOKMARK_END = "w:colLast"; + public static File replace(List data, byte[] word, File file) { - Map dataMap = data.stream() - .collect(Collectors.toMap(BookmarkReplaceDataModel::getName, b -> b)); try (XWPFDocument doc = new XWPFDocument(IoUtil.toStream(word))) { + // 收集文档中所有段落 List allParagraphs = new ArrayList<>(); collectionAllParagraphs(doc, allParagraphs); - + // 列表书签 List listMark = new ArrayList<>(); + // 表格内书签信息 + List tableBookmarkInfos = queryBookmarkInTable(doc); + // 所有待替换数据 + Map allDataMap = data.stream() + .collect(Collectors.toMap(BookmarkReplaceDataModel::getName, b -> b)); + // 常规替换数据 + Map dataMap = data.stream() + .filter(d -> { + List bookmarks = tableBookmarkInfos.stream() + .flatMap(info -> info.getBookmark().stream()) + .toList(); + return bookmarks.contains(d.getName()); + }) // 过滤表格中需要整行替换的书签 + .collect(Collectors.toMap(BookmarkReplaceDataModel::getName, b -> b)); // 处理段落中书签 for (XWPFParagraph paragraph : allParagraphs) { @@ -76,7 +97,9 @@ public class BookmarkExec { ctr.removeT(i); } CTText newTextNode = ctr.addNewT(); - newTextNode.setStringValue(model.getExtData().getValue()); + if (model.getExtData() instanceof TextExData exData) { + newTextNode.setStringValue(exData.getValue()); + } // 删除其余的run for (int i = startIdx + 1; i < endIdx; i++) { @@ -92,10 +115,26 @@ public class BookmarkExec { // 处理列表 for (String mk : listMark) { - handleListContent(doc, new String[]{"行1", "行2", "行3", "行4", "行5"}, mk); + if (allDataMap.containsKey(mk)) { + BookmarkReplaceDataModel model = allDataMap.get(mk); + if (model.getExtData() instanceof ListExData exData) { + handleListContent(doc, exData.getValue(), mk); + } + } } + // 处理表格 + for (TableBookmarkInfo tableBkInfo : tableBookmarkInfos) { + String masterBookmark = tableBkInfo.getMasterBookmark(); + if (allDataMap.containsKey(masterBookmark)) { + BookmarkReplaceDataModel model = allDataMap.get(masterBookmark); + if (model.getExtData() instanceof TableExData exData) { + handleTableContent(doc, tableBkInfo, exData.getValue()); + } + } + } + // 保存文件 File outFile = FileUtil.newFile(tmpDir + "gen/" + FileNameUtil.getName(file)); File parentDir = outFile.getParentFile(); if (!parentDir.exists()) { @@ -110,6 +149,126 @@ public class BookmarkExec { } } + /** + * 处理word中的表格,删除模板行,新增行 + */ + private static void handleTableContent(XWPFDocument doc, TableBookmarkInfo tableBkInfo, List> values) { + List tables = doc.getTables(); + int tableIdx = tableBkInfo.getTableIdx(); + XWPFTable table = tables.get(tableIdx); + int templateRowIdx = tableBkInfo.getTemplateRowIdx(); + + // 获取模板行 + XWPFTableRow templateRow = table.getRow(templateRowIdx); + + // 提取各列的书签名称 + List columnKeys = new ArrayList<>(); + for (XWPFTableCell cell : templateRow.getTableCells()) { + String bookmarkName = null; + // 遍历单元格的段落,查找第一个书签 + for (XWPFParagraph paragraph : cell.getParagraphs()) { + for (CTBookmark bookmark : paragraph.getCTP().getBookmarkStartList()) { + bookmarkName = bookmark.getName(); + break; // 取第一个书签 + } + if (bookmarkName != null) { + break; + } + } + if (bookmarkName == null) { + throw new IllegalArgumentException("模板行的单元格中未找到书签"); + } + columnKeys.add(bookmarkName); + } + + // 插入新行并填充数据 + int currentInsertPos = templateRowIdx; // 初始插入位置为模板行的索引 + for (Map dataRow : values) { + // 在指定位置插入新行 + XWPFTableRow newRow = table.insertNewTableRow(currentInsertPos); + // 复制模板行的每个单元格的格式并填充数据 + for (int i = 0; i < templateRow.getTableCells().size(); i++) { + XWPFTableCell templateCell = templateRow.getCell(i); + XWPFTableCell newCell = newRow.addNewTableCell(); + + // 复制单元格样式 + CTTcPr templateTcPr = templateCell.getCTTc().getTcPr(); + if (templateTcPr != null) { + newCell.getCTTc().setTcPr((CTTcPr) templateTcPr.copy()); + } + + // 设置单元格内容 + String key = columnKeys.get(i); + String value = dataRow.getOrDefault(key, ""); + // 清除单元格原有内容 + newCell.removeParagraph(0); // 移除默认创建的段落 + XWPFParagraph paragraph = newCell.addParagraph(); + XWPFRun run = paragraph.createRun(); + run.setText(value); + } + currentInsertPos++; // 插入位置后移 + } + + // 删除模板行(此时模板行的新索引为原索引加上插入的行数) + int finalTemplateRowIndex = templateRowIdx + values.size(); + table.removeRow(finalTemplateRowIndex); + } + + /** + * 查询word中表格中需要整行替换的书签 + */ + private static List queryBookmarkInTable(XWPFDocument doc) { + + List list = new ArrayList<>(); + List tables = doc.getTables(); + + // 表格 + for (XWPFTable table : tables) { + int idx = tables.indexOf(table); + TableBookmarkInfo info = new TableBookmarkInfo(); + List filterMark = new ArrayList<>(); + + // 行 + for (XWPFTableRow row : table.getRows()) { + int currentRowIdx = table.getRows().indexOf(row); + + // 单元格 + for (XWPFTableCell cell : row.getTableCells()) { + boolean isReplaceRow = false; + List paragraphs = cell.getParagraphs(); + for (XWPFParagraph paragraph : paragraphs) { + CTP ctp = paragraph.getCTP(); + List bookmark = ctp.getBookmarkStartList(); + + // 单元格内书签 + for (CTBookmark bk : bookmark) { + NamedNodeMap attr = bk.getDomNode().getAttributes(); + Optional startAttrOpt = Optional.ofNullable(attr.getNamedItem(TABLE_ROW_BOOKMARK_START)); + Optional endAttrOpt = Optional.ofNullable(attr.getNamedItem(TABLE_ROW_BOOKMARK_END)); + + // 是否是整行是书签,如果是,标记这一行为模板 + if (startAttrOpt.map(Node::getNodeValue).isPresent() && endAttrOpt.map(Node::getNodeValue).isPresent()) { + filterMark.add(bk.getName()); + isReplaceRow = true; + info.setTableIdx(idx); + info.setTemplateRowIdx(currentRowIdx); + info.setMasterBookmark(bk.getName()); + } + } + + // 收集模板行的所有书签 + if (isReplaceRow) { + filterMark.addAll(bookmark.stream().map(CTBookmark::getName).collect(Collectors.toSet())); + info.setBookmark(filterMark); + list.add(info); + } + } + } + } + } + return list; + } + /** * 查询本段中的书签信息 * @@ -138,6 +297,9 @@ public class BookmarkExec { return new ArrayList<>(set); } + /** + * 查找书签在段落中包含的位置 + */ private static void queryBookmarkIdx(XWPFParagraph paragraph, BookmarkInfo info) { List runs = paragraph.getRuns(); boolean foundStart = false; @@ -198,9 +360,6 @@ public class BookmarkExec { /** * 获取文档中所有段落 - * - * @param doc 文档 - * @param allParagraphs 段落 */ private static void collectionAllParagraphs(XWPFDocument doc, List allParagraphs) { @@ -241,9 +400,9 @@ public class BookmarkExec { } /** - * 处理行新增 + * 处理列表行新增 */ - public static void handleListContent(XWPFDocument doc, String[] newListItems, String bookmarkName) { + public static void handleListContent(XWPFDocument doc, List newListItems, String bookmarkName) { // 查找书签所在的段落 XWPFParagraph bookmarkParagraph = findBookmarkParagraph(doc, bookmarkName); if (bookmarkParagraph == null) { @@ -316,7 +475,7 @@ public class BookmarkExec { } } - private static void insertNewListItems(XWPFDocument doc, String[] items, int insertPos, + private static void insertNewListItems(XWPFDocument doc, List items, int insertPos, BigInteger numId, BigInteger ilvl, XWPFParagraph originalPara, XWPFRun xwpfRun) { // 获取插入位置的游标锚点 XmlCursor cursor = findInsertCursor(doc, insertPos); diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/enums/BookmarkType.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/enums/BookmarkType.java index 1cd9197..5d5ddf3 100644 --- a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/enums/BookmarkType.java +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/enums/BookmarkType.java @@ -18,7 +18,11 @@ public enum BookmarkType { PICTURE("PICTURE"), - PICTURE_DESC("PICTURE_DESC"); + PICTURE_DESC("PICTURE_DESC"), + + TABLE("TABLE"), + + LIST("LIST"),; private final String type; diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/AbstractExData.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/AbstractExData.java index 1af8f17..ee5bef1 100644 --- a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/AbstractExData.java +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/AbstractExData.java @@ -4,7 +4,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.wmyun.farmwork.word.core.enums.BookmarkType; +import com.wmyun.farmwork.word.core.model.ext.ListExData; import com.wmyun.farmwork.word.core.model.ext.PictureExData; +import com.wmyun.farmwork.word.core.model.ext.TableExData; import com.wmyun.farmwork.word.core.model.ext.TextExData; import lombok.Data; @@ -23,12 +25,12 @@ import lombok.Data; @JsonSubTypes({ @JsonSubTypes.Type(value = TextExData.class, name = "TEXT"), @JsonSubTypes.Type(value = PictureExData.class, name = "PICTURE"), - @JsonSubTypes.Type(value = PictureExData.class, name = "PICTURE_DESC") + @JsonSubTypes.Type(value = PictureExData.class, name = "PICTURE_DESC"), + @JsonSubTypes.Type(value = ListExData.class, name = "LIST"), + @JsonSubTypes.Type(value = TableExData.class, name = "TABLE") }) public class AbstractExData { @JsonProperty("type") private BookmarkType type; - - private String value; } diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/BookmarkInfo.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/BookmarkInfo.java index 58113b2..9fac6e6 100644 --- a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/BookmarkInfo.java +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/BookmarkInfo.java @@ -30,6 +30,9 @@ public class BookmarkInfo { // 是否是列表 private boolean listMark; + // 是否在表格中 + private boolean tableMark; + public BookmarkInfo(String bookmarkName, String bookmarkId) { this.bookmarkName = bookmarkName; this.bookmarkId = bookmarkId; diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/TableBookmarkInfo.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/TableBookmarkInfo.java new file mode 100644 index 0000000..f2587f3 --- /dev/null +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/TableBookmarkInfo.java @@ -0,0 +1,27 @@ +package com.wmyun.farmwork.word.core.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @Description: TODO + * @Date: 2025/3/7 16:35 + * @Created: by ZZSLL + */ + +@Data +@NoArgsConstructor +public class TableBookmarkInfo { + + // 表格在word中的index + private int tableIdx; + + private String masterBookmark; + + private List bookmark; + + private int templateRowIdx; +} diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/ListExData.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/ListExData.java new file mode 100644 index 0000000..0988489 --- /dev/null +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/ListExData.java @@ -0,0 +1,19 @@ +package com.wmyun.farmwork.word.core.model.ext; + +import com.wmyun.farmwork.word.core.model.AbstractExData; +import lombok.Data; + +import java.util.List; + +/** + * @Description: TODO + * @Date: 2025/3/7 16:52 + * @Created: by ZZSLL + */ + +@Data +public class ListExData extends AbstractExData { + + private List value; + +} diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/PictureExData.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/PictureExData.java index e31f935..6e31925 100644 --- a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/PictureExData.java +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/PictureExData.java @@ -28,16 +28,18 @@ public class PictureExData extends AbstractExData { private PictureProvideDataType dataType; + private String value; + @SneakyThrows public byte[] readAsByteArray() { if (PictureProvideDataType.BASE64.equals(dataType)) { - return Base64.getDecoder().decode(this.getValue()); + return Base64.getDecoder().decode(value); } if (PictureProvideDataType.DOWNLOAD_URL.equals(dataType)) { - return HttpUtil.downloadBytes(this.getValue()); + return HttpUtil.downloadBytes(value); } if (PictureProvideDataType.ABSOLUTE_PATH.equals(dataType)) { - return FileUtil.readBytes(this.getValue()); + return FileUtil.readBytes(value); } return null; } diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/TableExData.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/TableExData.java new file mode 100644 index 0000000..a9919e5 --- /dev/null +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/TableExData.java @@ -0,0 +1,18 @@ +package com.wmyun.farmwork.word.core.model.ext; + +import com.wmyun.farmwork.word.core.model.AbstractExData; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * @Description: TODO + * @Date: 2025/3/7 16:52 + * @Created: by ZZSLL + */ + +@Data +public class TableExData extends AbstractExData { + private List> value; +} diff --git a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/TextExData.java b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/TextExData.java index 9acbbb1..7470235 100644 --- a/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/TextExData.java +++ b/wmyun-framework/wmyun-spring-boot-starter-word/src/main/java/com/wmyun/farmwork/word/core/model/ext/TextExData.java @@ -15,6 +15,8 @@ import lombok.Data; @AllArgsConstructor public class TextExData extends AbstractExData { + private String value; + private Boolean bold; private Boolean italic;