|
@@ -0,0 +1,769 @@
|
|
|
+package com.yaoyicloud.render;
|
|
|
+
|
|
|
+import java.awt.image.BufferedImage;
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
+import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.net.URL;
|
|
|
+import java.text.DecimalFormat;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.regex.Matcher;
|
|
|
+import java.util.regex.Pattern;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+import javax.imageio.ImageIO;
|
|
|
+
|
|
|
+import org.apache.pdfbox.Loader;
|
|
|
+import org.apache.pdfbox.pdmodel.PDDocument;
|
|
|
+import org.apache.pdfbox.rendering.PDFRenderer;
|
|
|
+import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
|
|
|
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
|
|
+import org.apache.poi.xwpf.usermodel.XWPFHyperlinkRun;
|
|
|
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
|
|
+import org.apache.poi.xwpf.usermodel.XWPFRun;
|
|
|
+import org.apache.poi.xwpf.usermodel.XWPFTable;
|
|
|
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
|
|
+import org.apache.poi.xwpf.usermodel.XWPFTableRow;
|
|
|
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc;
|
|
|
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
|
|
|
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalJc;
|
|
|
+
|
|
|
+import com.deepoove.poi.XWPFTemplate;
|
|
|
+import com.deepoove.poi.config.Configure;
|
|
|
+import com.deepoove.poi.config.ConfigureBuilder;
|
|
|
+import com.deepoove.poi.data.ByteArrayPictureRenderData;
|
|
|
+import com.deepoove.poi.data.HyperlinkTextRenderData;
|
|
|
+import com.deepoove.poi.data.PictureType;
|
|
|
+import com.deepoove.poi.data.style.PictureStyle;
|
|
|
+import com.deepoove.poi.exception.RenderException;
|
|
|
+import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
|
|
+import com.deepoove.poi.policy.ParagraphRenderPolicy;
|
|
|
+import com.deepoove.poi.policy.PictureRenderPolicy;
|
|
|
+import com.deepoove.poi.policy.RenderPolicy;
|
|
|
+import com.deepoove.poi.policy.TextRenderPolicy;
|
|
|
+import com.deepoove.poi.template.ElementTemplate;
|
|
|
+import com.deepoove.poi.template.run.RunTemplate;
|
|
|
+import com.deepoove.poi.util.TableTools;
|
|
|
+import com.deepoove.poi.xwpf.BodyContainer;
|
|
|
+import com.deepoove.poi.xwpf.BodyContainerFactory;
|
|
|
+import com.yaoyicloud.tools.Util;
|
|
|
+
|
|
|
+import cn.hutool.core.lang.Pair;
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 抽象渲染器
|
|
|
+ *
|
|
|
+ */
|
|
|
+public abstract class AbstractNewRender {
|
|
|
+
|
|
|
+ /*
|
|
|
+ * 导出文件位置
|
|
|
+ */
|
|
|
+ protected final String cwd;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Docx结果文件路径
|
|
|
+ */
|
|
|
+ protected String docxResultPath;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * html结果文件路径
|
|
|
+ */
|
|
|
+ protected String htmlResultPath;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * pdf结果文件路径
|
|
|
+ */
|
|
|
+ protected String pdfResultPath;
|
|
|
+
|
|
|
+ public AbstractNewRender(String cwd) {
|
|
|
+ this.cwd = cwd;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Docx 渲染
|
|
|
+ *
|
|
|
+ *
|
|
|
+ * @param templateFileContent 模板内容
|
|
|
+ * @return 本地文件目录
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ @SuppressWarnings("checkstyle:ParameterNumber")
|
|
|
+ public final String renderDocx(Map<String, Object> dataMap, byte[] templateFileContent,
|
|
|
+ ConfigureBuilder builder, String relationId, String moduleType, Map<String, Object> specialMap)
|
|
|
+ throws IOException {
|
|
|
+
|
|
|
+ // 注: 报告模板的模板变量按照json序列化的结果命名
|
|
|
+ String basicPath = this.getBasicPath();
|
|
|
+ String reportImagePath = this.getReportImagePath();
|
|
|
+ String label = relationId + "_" + moduleType;
|
|
|
+ // word导出位置
|
|
|
+ String reportTempWordFile = basicPath + "/" + cwd + "/" + label + ".docx";
|
|
|
+ // 新增:创建文件夹
|
|
|
+ File parentDir = new File(basicPath + "/" + cwd);
|
|
|
+ if (!parentDir.exists()) {
|
|
|
+ boolean created = parentDir.mkdirs();
|
|
|
+ if (!created) {
|
|
|
+ throw new IOException("无法创建文件保存目录");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Configure config = builder.build();
|
|
|
+ XWPFTemplate template =
|
|
|
+ XWPFTemplate.compile(new ByteArrayInputStream(templateFileContent), config).render(dataMap);
|
|
|
+ ArrayList<String> templateDele = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, Object> entry : specialMap.entrySet()) {
|
|
|
+ String key = entry.getKey();
|
|
|
+ Object value = entry.getValue();
|
|
|
+ if (value.equals("")) {
|
|
|
+ templateDele.add(key);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Map<String, String> keywordMap = new HashMap<>();
|
|
|
+ keywordMap.put("suggestion", "建议");
|
|
|
+ keywordMap.put("score", "分");
|
|
|
+ XWPFDocument doc = template.getXWPFDocument();
|
|
|
+ List<XWPFTable> tables = doc.getTables();
|
|
|
+ XWPFTable xwpfTable = tables.get(tables.size() - 1);
|
|
|
+ for (int i = xwpfTable.getNumberOfRows() - 1; i >= 0; i--) {
|
|
|
+ XWPFTableRow row = xwpfTable.getRow(i);
|
|
|
+ StringBuilder rowText = new StringBuilder();
|
|
|
+ for (XWPFTableCell cell : row.getTableCells()) {
|
|
|
+ rowText.append(cell.getText());
|
|
|
+ }
|
|
|
+ for (String codeKeyword : templateDele) {
|
|
|
+ String templateKeyword = keywordMap.get(codeKeyword);
|
|
|
+ if (rowText.toString().contains(templateKeyword)) {
|
|
|
+ xwpfTable.removeRow(i);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ template.writeToFile(reportTempWordFile);
|
|
|
+ template.close();
|
|
|
+
|
|
|
+ this.docxResultPath = reportTempWordFile;
|
|
|
+ return this.docxResultPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * docx转换为pdf
|
|
|
+ *
|
|
|
+ * @return 本地文件目录
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ public final String fromDocxToPdf() throws IOException {
|
|
|
+ // 转换 docxResultPath 为 html
|
|
|
+ // 加工html
|
|
|
+ // 把 html 文件转成 pdf
|
|
|
+ fromDocxToHtml();
|
|
|
+ this.pdfResultPath = fromHtmlToPdf();
|
|
|
+ return this.pdfResultPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * docx转换为Html
|
|
|
+ *
|
|
|
+ * @return 本地文件目录
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ public final String fromDocxToHtml() throws IOException {
|
|
|
+ // 转换 docxResultPath 为 html
|
|
|
+ // 加工html
|
|
|
+ this.htmlResultPath = cwd + "/1.html";
|
|
|
+ return this.htmlResultPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * html转换为pdf
|
|
|
+ *
|
|
|
+ * @return 本地文件目录
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ public final String fromHtmlToPdf() throws IOException {
|
|
|
+ // 转换 htmlResultPath 为 pdf
|
|
|
+ this.pdfResultPath = cwd + "/1.pdf";
|
|
|
+ return this.pdfResultPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected abstract String getBasicPath() throws IOException;
|
|
|
+
|
|
|
+ protected abstract String getReportImagePath();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 针对于protobuf传递来的数据处理
|
|
|
+ *
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ protected RenderPolicy indicatorsRenderPolicyToProtobuf() {
|
|
|
+ return new LoopRowTableRenderPolicy() {
|
|
|
+ @Override
|
|
|
+ public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
|
|
|
+ // 获取模板中的变量如:[name]
|
|
|
+ ArrayList<String> strings = processElement(eleTemplate, data);
|
|
|
+ List<Map<String, Object>> processedData = null;
|
|
|
+ if (null != data && data instanceof Iterable) {
|
|
|
+ processedData = (List<Map<String, Object>>) data;
|
|
|
+ processedData.forEach(map -> {
|
|
|
+ for (String string : strings) {
|
|
|
+ Object o = map.get(string);
|
|
|
+ if (null == o || "".equals(o)) {
|
|
|
+ map.put(string, "-");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 调用父类渲染处理后的数据
|
|
|
+ super.render(eleTemplate, processedData, template);
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("checkstyle:NestedForDepth")
|
|
|
+ private ArrayList<String> processElement(ElementTemplate eleTemplate, Object data) {
|
|
|
+ RunTemplate runTemplate = (RunTemplate) eleTemplate;
|
|
|
+ XWPFRun run = runTemplate.getRun();
|
|
|
+ XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
|
|
|
+ XWPFTable table = tagCell.getTableRow().getTable();
|
|
|
+ StringBuilder textBuilder = new StringBuilder();
|
|
|
+
|
|
|
+ for (XWPFTableRow row : table.getRows()) {
|
|
|
+ // 遍历行中的每一个单元格
|
|
|
+ for (XWPFTableCell cell : row.getTableCells()) {
|
|
|
+ // 提取单元格内的所有文本(包含段落、Run 等)
|
|
|
+ for (XWPFParagraph para : cell.getParagraphs()) {
|
|
|
+ for (XWPFRun r : para.getRuns()) {
|
|
|
+ textBuilder.append(r.getText(0)); // 获取 Run 的文本
|
|
|
+ }
|
|
|
+ }
|
|
|
+ textBuilder.append(" ");
|
|
|
+ }
|
|
|
+ textBuilder.append(" ");
|
|
|
+ }
|
|
|
+ ArrayList<String> strings = new ArrayList<>();
|
|
|
+ String string = textBuilder.toString();
|
|
|
+ String[] split = string.split(" ");
|
|
|
+ for (String s : split) {
|
|
|
+ Pattern pattern = Pattern.compile("\\[(.*?)\\]");
|
|
|
+ Matcher matcher = pattern.matcher(s);
|
|
|
+ while (matcher.find()) {
|
|
|
+ String contentInBracket = matcher.group(1);
|
|
|
+ strings.add(contentInBracket);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return strings;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 针对于json格式全量传输数据的列表处理 将默认值null转为“-”
|
|
|
+ *
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ protected RenderPolicy indicatorsRenderPolicy() {
|
|
|
+ return new LoopRowTableRenderPolicy() {
|
|
|
+ @Override
|
|
|
+ public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
|
|
|
+ // 处理数据中的null值
|
|
|
+ Object processedData = processData(data);
|
|
|
+ // 调用父类渲染处理后的数据
|
|
|
+ super.render(eleTemplate, processedData, template);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Object processData(Object data) {
|
|
|
+ if (data == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data instanceof List) {
|
|
|
+ // 处理List类型数据
|
|
|
+ return ((List<?>) data).stream()
|
|
|
+ .map(this::processItem)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Object processItem(Object item) {
|
|
|
+ if (item == null) {
|
|
|
+ return "-";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果元素是Map,处理Map中的null值
|
|
|
+ if (item instanceof Map) {
|
|
|
+ Map<?, ?> map = (Map<?, ?>) item;
|
|
|
+ return map.entrySet().stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ Map.Entry::getKey,
|
|
|
+ e -> e.getValue() == null ? "-" : e.getValue()));
|
|
|
+ }
|
|
|
+
|
|
|
+ return item;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 这些render policy类都应当是共享的 重要设计假设: data的类型cast都可以建立在json通用反序列化后的基本类型基础上。
|
|
|
+ */
|
|
|
+ protected RenderPolicy pictureRenderPolicy() {
|
|
|
+ return new PictureRenderPolicy() {
|
|
|
+ @Override
|
|
|
+ public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Object> mpData = (Map<String, Object>) data;
|
|
|
+ String filename = mpData.getOrDefault("fileName", "").toString().trim();
|
|
|
+ String url = mpData.getOrDefault("fileUri", "").toString();
|
|
|
+ float targetWidth = 456.5f;
|
|
|
+
|
|
|
+ if (StrUtil.isBlank(filename)) {
|
|
|
+ // uri render when no filename
|
|
|
+ TextRenderPolicy.Helper.renderTextRun(((RunTemplate) eleTemplate).getRun(),
|
|
|
+ new HyperlinkTextRenderData(url, url));
|
|
|
+ } else if (filename.endsWith(".pdf")) {
|
|
|
+ // pdf render, replace data with bytestream
|
|
|
+ PDDocument document = null;
|
|
|
+ try {
|
|
|
+ document = Loader.loadPDF(new URL(url).openStream().readAllBytes());
|
|
|
+ PDFRenderer renderer = new PDFRenderer(document);
|
|
|
+
|
|
|
+ XWPFRun run = ((RunTemplate) eleTemplate).getRun();
|
|
|
+ BodyContainer bodyContainer = BodyContainerFactory.getBodyContainer(run);
|
|
|
+ // 每页一张图片
|
|
|
+ for (int i = 0; i < document.getNumberOfPages(); i++) {
|
|
|
+ // pdf 转 jpeg
|
|
|
+ BufferedImage image = renderer.renderImageWithDPI(i, 150);
|
|
|
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
|
+ ImageIO.write(image, "jpg", stream);
|
|
|
+
|
|
|
+ // 准备docx元素
|
|
|
+ if (i == 0) {
|
|
|
+ run.setText("", 0);
|
|
|
+ } else {
|
|
|
+ run = bodyContainer.insertNewParagraph(run).createRun();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 准备POI-TL数据
|
|
|
+ ByteArrayPictureRenderData picData =
|
|
|
+ new ByteArrayPictureRenderData(stream.toByteArray(), PictureType.JPEG);
|
|
|
+ Pair<Integer, Integer> targetSize =
|
|
|
+ calculateTargetSize(image.getWidth(), image.getHeight(), targetWidth);
|
|
|
+ PictureStyle style = new PictureStyle();
|
|
|
+ style.setWidth(targetSize.getKey());
|
|
|
+ style.setHeight(targetSize.getValue());
|
|
|
+ picData.setPictureStyle(style);
|
|
|
+ Helper.renderPicture(run, picData);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RenderException(
|
|
|
+ "AttachmentRenderPolicy for " + eleTemplate + " error: " + e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ if (document != null) {
|
|
|
+ try {
|
|
|
+ document.close();
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ BufferedImage image = null;
|
|
|
+ ByteArrayOutputStream stream = null;
|
|
|
+ try {
|
|
|
+ image = ImageIO.read(new URL(url));
|
|
|
+ stream = new ByteArrayOutputStream();
|
|
|
+ ImageIO.write(image, getFileExtension(filename), stream);
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new RenderException(
|
|
|
+ "AttachmentRenderPolicy for " + eleTemplate + " error: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+
|
|
|
+ ByteArrayPictureRenderData picData =
|
|
|
+ new ByteArrayPictureRenderData(stream.toByteArray(), PictureType.suggestFileType(filename));
|
|
|
+ Pair<Integer, Integer> targetSize =
|
|
|
+ calculateTargetSize(image.getWidth(), image.getHeight(), targetWidth);
|
|
|
+ PictureStyle style = new PictureStyle();
|
|
|
+ style.setWidth(targetSize.getKey());
|
|
|
+ style.setHeight(targetSize.getValue());
|
|
|
+ picData.setPictureStyle(style);
|
|
|
+ super.render(eleTemplate, picData, template);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算目标尺寸(提取为内部方法)
|
|
|
+ */
|
|
|
+ private Pair<Integer, Integer> calculateTargetSize(int originWidth, int originHeight, float targetWidth) {
|
|
|
+ float targetHeight = (originHeight * targetWidth) / originWidth;
|
|
|
+ if (targetHeight > 645) {
|
|
|
+ targetHeight = 645;
|
|
|
+ targetWidth = (originWidth * targetHeight) / originHeight;
|
|
|
+ }
|
|
|
+ return Pair.of(Math.round(targetWidth), Math.round(targetHeight));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取文件扩展名
|
|
|
+ */
|
|
|
+ private String getFileExtension(String filename) {
|
|
|
+ int dotIndex = filename.lastIndexOf('.');
|
|
|
+ return dotIndex > 0 ? filename.substring(dotIndex + 1).toLowerCase() : "jpg";
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 超链接列表策略
|
|
|
+ *
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ protected RenderPolicy hyperlinkRenderPolicy() {
|
|
|
+ return new ParagraphRenderPolicy() {
|
|
|
+ @Override
|
|
|
+ public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
|
|
|
+ // 获取当前运行的
|
|
|
+ XWPFParagraph paragraph = ((RunTemplate) eleTemplate).getRun().getParagraph();
|
|
|
+ int pos = paragraph.getRuns().indexOf(((RunTemplate) eleTemplate).getRun());
|
|
|
+ paragraph.removeRun(pos);
|
|
|
+ paragraph.removeRun(pos);
|
|
|
+ if (data instanceof String) {
|
|
|
+ XWPFHyperlinkRun hyperlinkRun = paragraph.createHyperlinkRun(data.toString());
|
|
|
+ hyperlinkRun.setText(data.toString());
|
|
|
+ hyperlinkRun.setColor("0000FF");
|
|
|
+ hyperlinkRun.setUnderline(UnderlinePatterns.SINGLE);
|
|
|
+ hyperlinkRun.setFontFamily("思源黑体 Medium");
|
|
|
+ hyperlinkRun.setFontSize(10.5);
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ protected RenderPolicy getScoreRenderPolicy() {
|
|
|
+ RenderPolicy policy = new LoopRowTableRenderPolicy() {
|
|
|
+ @SuppressWarnings("checkstyle:NestedForDepth")
|
|
|
+ @Override
|
|
|
+ public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
|
|
|
+ // 检查数据是否为空
|
|
|
+ if (data == null || (data instanceof Collection && ((Collection<?>) data).isEmpty())) {
|
|
|
+ // 数据为空时,删除整个表格
|
|
|
+ // removeTemplateTable(eleTemplate, template);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ super.render(eleTemplate, data, template);
|
|
|
+ XWPFDocument doc = template.getXWPFDocument();
|
|
|
+ for (XWPFTable table : doc.getTables()) {
|
|
|
+ boolean isTargetTable = false;
|
|
|
+ // 判断是否为目标表格:通过检查表格第一行是否包含货物明细相关表头关键词
|
|
|
+ List<String> headerKeywords = List.of("删除");
|
|
|
+ XWPFTableRow firstRow = table.getRow(0);
|
|
|
+ if (firstRow != null) {
|
|
|
+ for (XWPFTableCell cell : firstRow.getTableCells()) {
|
|
|
+ for (XWPFParagraph para : cell.getParagraphs()) {
|
|
|
+ for (XWPFRun run : para.getRuns()) {
|
|
|
+ String text = run.text().trim();
|
|
|
+ if (headerKeywords.contains(text)) {
|
|
|
+ isTargetTable = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isTargetTable) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isTargetTable) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (isTargetTable) {
|
|
|
+ // 删除表头(假设表头是第一行)
|
|
|
+ if (table.getRows().size() > 0) {
|
|
|
+ table.removeRow(0);
|
|
|
+ }
|
|
|
+ // 合并第一列相同内容单元格并居中
|
|
|
+ mergeAndCenterFirstColumn(table);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void mergeAndCenterFirstColumn(XWPFTable table) {
|
|
|
+ if (table.getNumberOfRows() < 2) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int startRow = 0;
|
|
|
+ String currentValue = getCellText(table.getRow(0)).get(0);
|
|
|
+
|
|
|
+ for (int i = 1; i < table.getNumberOfRows(); i++) {
|
|
|
+ List<String> rowValues = getCellText(table.getRow(i));
|
|
|
+ if (rowValues.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ String firstCellValue = rowValues.get(0);
|
|
|
+ if (firstCellValue.equals(currentValue)) {
|
|
|
+ // 继续查找相同值的行
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ // 合并从startRow到i-1行的第一列单元格
|
|
|
+ if (i - 1 > startRow) {
|
|
|
+ TableTools.mergeCellsVertically(table, 0, startRow, i - 1);
|
|
|
+ // 设置合并后单元格内容居中
|
|
|
+ setCellCenterAlignment(table.getRow(startRow).getCell(0));
|
|
|
+ }
|
|
|
+ startRow = i;
|
|
|
+ currentValue = firstCellValue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理最后一组相同值
|
|
|
+ if (table.getNumberOfRows() - 1 > startRow) {
|
|
|
+ TableTools.mergeCellsVertically(table, 0, startRow, table.getNumberOfRows() - 1);
|
|
|
+ setCellCenterAlignment(table.getRow(startRow).getCell(0));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return policy;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> getCellText(XWPFTableRow row) {
|
|
|
+ List<String> texts = new ArrayList<>();
|
|
|
+ if (row == null) {
|
|
|
+ return texts;
|
|
|
+ }
|
|
|
+ for (XWPFTableCell cell : row.getTableCells()) {
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (XWPFParagraph para : cell.getParagraphs()) {
|
|
|
+ for (XWPFRun run : para.getRuns()) {
|
|
|
+ sb.append(run.text());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ texts.add(sb.toString().trim());
|
|
|
+ }
|
|
|
+ return texts;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void setCellCenterAlignment(XWPFTableCell cell) {
|
|
|
+ CTTc cttc = cell.getCTTc();
|
|
|
+ CTTcPr ctPr = cttc.addNewTcPr();
|
|
|
+ ctPr.addNewVAlign().setVal(STVerticalJc.CENTER);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 这些render policy类都应当是共享的 重要设计假设: data的类型cast都可以建立在json通用反序列化后的基本类型基础上。
|
|
|
+ */
|
|
|
+ public class LoopColumnStaticTableRenderPolicy implements RenderPolicy {
|
|
|
+
|
|
|
+ private String prefix;
|
|
|
+ private String suffix;
|
|
|
+ private boolean onSameLine;
|
|
|
+ private boolean reverse;
|
|
|
+ private int valColIndex;
|
|
|
+
|
|
|
+ public LoopColumnStaticTableRenderPolicy() {
|
|
|
+ this(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ public LoopColumnStaticTableRenderPolicy(boolean onSameLine) {
|
|
|
+ this("[", "]", onSameLine);
|
|
|
+ }
|
|
|
+
|
|
|
+ public LoopColumnStaticTableRenderPolicy(String prefix, String suffix) {
|
|
|
+ this(prefix, suffix, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine) {
|
|
|
+ this(prefix, suffix, onSameLine, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine, boolean reverse) {
|
|
|
+ this(prefix, suffix, onSameLine, false, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ public LoopColumnStaticTableRenderPolicy(String prefix, String suffix, boolean onSameLine, boolean reverse,
|
|
|
+ int valRowIndex) {
|
|
|
+ this.prefix = prefix;
|
|
|
+ this.suffix = suffix;
|
|
|
+ this.onSameLine = onSameLine;
|
|
|
+ this.reverse = reverse;
|
|
|
+ this.valColIndex = valRowIndex;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
|
|
|
+ RunTemplate runTemplate = (RunTemplate) eleTemplate;
|
|
|
+ XWPFRun run = runTemplate.getRun();
|
|
|
+ try {
|
|
|
+ if (!TableTools.isInsideTable(run)) {
|
|
|
+ throw new IllegalStateException(
|
|
|
+ "The template tag " + runTemplate.getSource() + " must be inside a table");
|
|
|
+ }
|
|
|
+ XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
|
|
|
+ XWPFTable table = tagCell.getTableRow().getTable();
|
|
|
+ run.setText("", 0);
|
|
|
+
|
|
|
+ int templateColIndex = getTemplateColIndex(tagCell);
|
|
|
+ // 模版变量列总是写在左边
|
|
|
+ int minIndex = templateColIndex;
|
|
|
+ int maxIndex = table.getRows().get(valColIndex).getTableCells().size() - 1;
|
|
|
+ int currIndex = reverse ? maxIndex : minIndex;
|
|
|
+ int indexDelta = reverse ? -1 : 1;
|
|
|
+
|
|
|
+ // 目前expression当作数据Map的key来对待,将来可以当作POI-TL变量一致化的处理
|
|
|
+ Map<Integer, String> idx2Expression = new HashMap<>();
|
|
|
+ for (int i = 0; i < table.getRows().size(); i++) {
|
|
|
+ XWPFTableCell cell = table.getRows().get(i).getCell(templateColIndex);
|
|
|
+ String text = cell.getText().trim();
|
|
|
+ if (text.startsWith(prefix) && text.endsWith(suffix)) {
|
|
|
+ idx2Expression.put(i, text.substring(1, text.length() - 1));
|
|
|
+ cell.setText("");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ int rowSize = table.getRows().size();
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
|
|
|
+
|
|
|
+ for (Map<String, Object> realData : mpData) {
|
|
|
+ for (int i = 0; i < rowSize; i++) {
|
|
|
+ if (!idx2Expression.containsKey(i)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ XWPFTableRow row = table.getRow(i);
|
|
|
+ XWPFTableCell valueCell = row.getCell(currIndex);
|
|
|
+ String valStr = realData.getOrDefault(idx2Expression.get(i), "-").toString();
|
|
|
+ valueCell.setText(valStr);
|
|
|
+ }
|
|
|
+ currIndex += indexDelta;
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RenderException("HackLoopTable for " + eleTemplate + "error: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private int getTemplateColIndex(XWPFTableCell tagCell) {
|
|
|
+ return onSameLine ? Util.getColIndexOfFirstRow(tagCell) : (Util.getColIndexOfFirstRow(tagCell) + 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public class LoopRowCutAndMergeFirstColTableRenderPolicy extends LoopRowTableRenderPolicy {
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
|
|
|
+ RunTemplate runTemplate = (RunTemplate) eleTemplate;
|
|
|
+ XWPFRun run = runTemplate.getRun();
|
|
|
+ XWPFTable table = null;
|
|
|
+ try {
|
|
|
+ if (!TableTools.isInsideTable(run)) {
|
|
|
+ throw new IllegalStateException(
|
|
|
+ "The template tag " + runTemplate.getSource() + " must be inside a table");
|
|
|
+ }
|
|
|
+ // Reserve the first two rows
|
|
|
+ XWPFTableCell tagCell = (XWPFTableCell) ((XWPFParagraph) run.getParent()).getBody();
|
|
|
+ table = tagCell.getTableRow().getTable();
|
|
|
+ for (int i = table.getNumberOfRows() - 1; i > 1; i--) {
|
|
|
+ table.removeRow(i);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RenderException(
|
|
|
+ "LoopRowCutAndMergeFirstColTable for " + eleTemplate + " error: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // in case data not sorted by rank
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
|
|
|
+ mpData.sort((a, b) -> Integer.valueOf(a.getOrDefault("rank", "0").toString())
|
|
|
+ - Integer.valueOf(b.getOrDefault("rank", "0").toString()));
|
|
|
+
|
|
|
+ super.render(eleTemplate, data, template);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // merge the first column
|
|
|
+ Util.mergeFirstNColSimple(table, 1, 0);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RenderException(
|
|
|
+ "LoopRowCutAndMergeFirstColTable for " + eleTemplate + " error: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public class LoopRowIncludeStatisticsTableRenderPolicy extends LoopRowTableRenderPolicy {
|
|
|
+
|
|
|
+ private String valueTag;
|
|
|
+ // 存储计算得到的平均值
|
|
|
+ private static final Map<String, String> AVERAGE_VALUES = new HashMap<>();
|
|
|
+
|
|
|
+ public LoopRowIncludeStatisticsTableRenderPolicy(String valueTag) {
|
|
|
+ this.valueTag = valueTag;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> mpData = (List<Map<String, Object>>) data;
|
|
|
+
|
|
|
+ for (Map<String, Object> row : mpData) {
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<String> values = (List<String>) row.get(valueTag);
|
|
|
+
|
|
|
+ // 检查是否有百分号结尾的值
|
|
|
+ boolean hasPercentage = values.stream().anyMatch(v -> v != null && v.endsWith("%"));
|
|
|
+
|
|
|
+ // 处理百分号并转换为数值
|
|
|
+ List<Double> processedValues = values.stream()
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .map(v -> {
|
|
|
+ if (v.endsWith("%")) {
|
|
|
+ return v.substring(0, v.length() - 1);
|
|
|
+ }
|
|
|
+ return v;
|
|
|
+ })
|
|
|
+ .map(v -> {
|
|
|
+ try {
|
|
|
+ return Double.valueOf(v);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return Double.NaN;
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .filter(v -> !Double.isNaN(v))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 计算平均值并保留两位小数
|
|
|
+ double avg = processedValues.stream()
|
|
|
+ .mapToDouble(Double::doubleValue)
|
|
|
+ .average()
|
|
|
+ .orElse(Double.NaN);
|
|
|
+
|
|
|
+ // 格式化结果
|
|
|
+ String formattedAvg;
|
|
|
+ if (Double.isNaN(avg)) {
|
|
|
+ formattedAvg = "-"; // 处理无有效数据的情况
|
|
|
+ } else {
|
|
|
+ DecimalFormat df = new DecimalFormat("#0.00"); // 保留两位小数
|
|
|
+ formattedAvg = df.format(avg);
|
|
|
+ if (hasPercentage) {
|
|
|
+ formattedAvg += "%"; // 添加百分号
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ row.put("avg", formattedAvg);
|
|
|
+ // AVERAGE_VALUES.put(row.get(""), formattedAvg);
|
|
|
+ }
|
|
|
+
|
|
|
+ super.render(eleTemplate, data, template);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|