From a1f3106c1e47f542107b86122ec45467524b1cef Mon Sep 17 00:00:00 2001 From: gjh <1421wake> Date: Tue, 2 Sep 2025 16:29:09 +0800 Subject: [PATCH] =?UTF-8?q?word=E6=A8=A1=E6=9D=BF=E5=AF=BC=E5=87=BAdemo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-modules/ruoyi-demo/pom.xml | 5 + .../org/dromara/demo/domain/CheckItem.java | 82 +++++ .../org/dromara/demo/domain/ReportData.java | 67 ++++ .../demo/domain/WordReportExporter.java | 303 ++++++++++++++++++ 4 files changed, 457 insertions(+) create mode 100644 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/CheckItem.java create mode 100644 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/ReportData.java create mode 100644 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/WordReportExporter.java diff --git a/ruoyi-modules/ruoyi-demo/pom.xml b/ruoyi-modules/ruoyi-demo/pom.xml index 119fe61..1140c33 100644 --- a/ruoyi-modules/ruoyi-demo/pom.xml +++ b/ruoyi-modules/ruoyi-demo/pom.xml @@ -16,6 +16,11 @@ + + org.apache.poi + poi-ooxml + 5.2.4 + diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/CheckItem.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/CheckItem.java new file mode 100644 index 0000000..d29b521 --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/CheckItem.java @@ -0,0 +1,82 @@ +package org.dromara.demo.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 检查项实体类(平铺结构中的最小单位) + * 每个字段说明如下: + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CheckItem { + + /** + * 检查项目(用于合并单元格) + * 示例值:"01 站房环境保障情况" + * 多个 CheckItem 可共享相同的 checkProject,表示属于同一类别 + */ + private String checkProject; + + /** + * 具体检查子项描述 + * 示例值:"站房清洁与规范管理" + * 表示该项目下的具体考核条目 + */ + private String checkItem; + + /** + * 该项满分分数 + * 示例值:2 + * 表示该检查项的总分为 2 分 + */ + private Integer itemScore; + + /** + * 实际获得分数 + * 示例值:1 + * 表示本次检查实际得分为 1 分 + */ + private Integer obtainedScore; + + /** + * 评分细则列表(前端传入的多条依据) + * 示例值: + * [ + * "1)站房环境脏,有明显灰尘和异味;", + * "2)物品摆放不整齐;" + * ] + * 每条为独立字符串,导出时需换行显示 + */ + private List scoringBasis; + + /** + * 扣分规则说明 + * 示例值:"一项不合格扣0.5分,扣完为止" + * 用于说明扣分逻辑,通常显示在评分细则下方 + */ + private String deductionRule; + + /** + * 格式化评分说明(用于导出 Word) + * 将评分细则和扣分规则拼接成带换行符的字符串 + * + * @return 格式化后的完整说明文本 + */ + public String formatScoringExplanation() { + StringBuilder sb = new StringBuilder(); + if (this.scoringBasis != null) { + for (String basis : this.scoringBasis) { + sb.append(basis).append("\n"); + } + } + if (this.deductionRule != null && !this.deductionRule.trim().isEmpty()) { + sb.append(this.deductionRule); + } + return sb.toString().trim(); + } +} diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/ReportData.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/ReportData.java new file mode 100644 index 0000000..33dbce0 --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/ReportData.java @@ -0,0 +1,67 @@ +package org.dromara.demo.domain; + +import lombok.Data; +import java.time.LocalDate; +import java.util.List; + +/** + * 监理服务现场技术核查评分报告数据模型 + * 注意:checkItems 是平铺列表,每个 CheckItem 都包含 checkProject 字段 + * 用于后续按 checkProject 值进行合并单元格(Word 或前端表格) + */ +@Data +public class ReportData { + + /** 报告标题,如:监理服务现场技术核查评分报告 */ + private String reportTitle; + + /** 站点名称,如:浦东新区张江空气质量监测站 */ + private String siteName; + + /** 所属区或管委会,如:浦东新区 */ + private String district; + + /** 所属街道、乡或镇,如:张江镇 */ + private String street; + + /** 运维单位,如:上海环保科技有限公司 */ + private String operationUnit; + + /** 检查时间,格式:yyyy-MM-dd */ + private LocalDate checkTime; + + /** 检查人员,多人用顿号或逗号分隔,如:张三、李四 */ + private String checkPersonnel; + + /** + * 检查项列表(平铺结构) + * 每个 CheckItem 自带 checkProject 字段 + * 多个 CheckItem 可能具有相同的 checkProject,用于合并单元格 + */ + private List checkItems; + + /** 总分 */ + private Integer totalScore; + + /** 备注 */ + private String remarks; + + public int getTotalPossibleScore() { + return getCheckItems().stream() + .mapToInt(CheckItem::getItemScore) + .sum(); + } + + public int getTotalObtainedScore() { + return getCheckItems().stream() + .mapToInt(CheckItem::getObtainedScore) + .sum(); + } + + // 可选:返回格式化字符串 "85/100" + public String getFormattedTotalScore() { + int obtained = getTotalObtainedScore(); + int total = getTotalPossibleScore(); + return obtained + "/" + total; + } +} diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/WordReportExporter.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/WordReportExporter.java new file mode 100644 index 0000000..9f31954 --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/domain/WordReportExporter.java @@ -0,0 +1,303 @@ +package org.dromara.demo.domain; + +import cn.hutool.json.JSONUtil; +import org.apache.poi.xwpf.usermodel.*; +import org.dromara.demo.domain.ReportData; +import org.dromara.demo.domain.CheckItem; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.File; +import java.math.BigInteger; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class WordReportExporter { + + // 获取桌面路径(跨平台) + private static String getDesktopPath() { + return "D:\\Users\\14212\\Desktop"; + } + + public static void export(ReportData reportData) throws IOException { + System.out.println(JSONUtil.toJsonStr(reportData)); + + String desktopPath = getDesktopPath(); + String filename = "数据购买监理服务现场技术核查评分表.docx"; + String fullPath = desktopPath + File.separator + filename; + + XWPFDocument document = new XWPFDocument(); + + // 1. 添加标题 + XWPFParagraph titlePara = document.createParagraph(); + titlePara.setAlignment(ParagraphAlignment.CENTER); + XWPFRun titleRun = titlePara.createRun(); + titleRun.setText(reportData.getReportTitle()); + titleRun.setBold(true); + titleRun.setFontSize(16); + titleRun.setFontFamily("宋体"); + + // 2. 添加基本信息 + addBasicInfo(document, reportData); + + // 3. 添加评分表格 + addScoreTable(document, reportData); + + // 4. 添加总分和备注 + addTotalAndRemarks(document, reportData); + + // 保存到桌面 + try (FileOutputStream out = new FileOutputStream(fullPath)) { + document.write(out); + } + document.close(); + + System.out.println("✅ 报告已成功导出到桌面:"); + System.out.println("📄 " + fullPath); + } + + private static void addBasicInfo(XWPFDocument document, ReportData reportData) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + // 站点名称 + XWPFParagraph p = document.createParagraph(); + p.setSpacingAfter(10); + + XWPFRun label = p.createRun(); + label.setFontFamily("宋体"); + label.setFontSize(12); + label.setBold(true); + label.setText("站点名称:"); + + XWPFRun value = p.createRun(); + value.setFontFamily("宋体"); + value.setFontSize(12); + value.setBold(false); + value.setText(reportData.getSiteName()); + + // 区(管委会) + p = document.createParagraph(); + p.setSpacingAfter(10); + + label = p.createRun(); + label.setFontFamily("宋体"); + label.setFontSize(12); + label.setBold(true); + label.setText("区(管委会):"); + + value = p.createRun(); + value.setFontFamily("宋体"); + value.setFontSize(12); + value.setBold(false); + value.setText(reportData.getDistrict()); + + // 街道(乡、镇) + p = document.createParagraph(); + p.setSpacingAfter(10); + + label = p.createRun(); + label.setFontFamily("宋体"); + label.setFontSize(12); + label.setBold(true); + label.setText("街道(乡、镇):"); + + value = p.createRun(); + value.setFontFamily("宋体"); + value.setFontSize(12); + value.setBold(false); + value.setText(reportData.getStreet()); + + // 运维单位 + p = document.createParagraph(); + p.setSpacingAfter(10); + + label = p.createRun(); + label.setFontFamily("宋体"); + label.setFontSize(12); + label.setBold(true); + label.setText("运维单位:"); + + value = p.createRun(); + value.setFontFamily("宋体"); + value.setFontSize(12); + value.setBold(false); + value.setText(reportData.getOperationUnit()); + + // 检查时间 + p = document.createParagraph(); + p.setSpacingAfter(10); + + label = p.createRun(); + label.setFontFamily("宋体"); + label.setFontSize(12); + label.setBold(true); + label.setText("检查时间:"); + + value = p.createRun(); + value.setFontFamily("宋体"); + value.setFontSize(12); + value.setBold(false); + value.setText(reportData.getCheckTime().format(formatter)); + + // 检查人员 + p = document.createParagraph(); + p.setSpacingAfter(10); + + label = p.createRun(); + label.setFontFamily("宋体"); + label.setFontSize(12); + label.setBold(true); + label.setText("检查人员:"); + + value = p.createRun(); + value.setFontFamily("宋体"); + value.setFontSize(12); + value.setBold(false); + value.setText(reportData.getCheckPersonnel()); + } + + private static void addScoreTable(XWPFDocument document, ReportData reportData) { + List items = reportData.getCheckItems(); + if (items == null || items.isEmpty()) return; + + XWPFTable table = document.createTable(); + CTTblWidth width = table.getCTTbl().getTblPr().addNewTblW(); + width.setW(BigInteger.valueOf(9000)); + width.setType(STTblWidth.DXA); + + // 表头 + XWPFTableRow header = table.getRow(0); + setCellTextAndStyle(header.getCell(0), "检查项目", true); + header.addNewTableCell(); + setCellTextAndStyle(header.getCell(1), "检查子项", true); + header.addNewTableCell(); + setCellTextAndStyle(header.getCell(2), "满分分数", true); + header.addNewTableCell(); + setCellTextAndStyle(header.getCell(3), "实际得分", true); + header.addNewTableCell(); + setCellTextAndStyle(header.getCell(4), "评分说明", true); + + // 按 checkProject 分组 + Map> grouped = items.stream() + .collect(Collectors.groupingBy(CheckItem::getCheckProject)); + + for (Map.Entry> entry : grouped.entrySet()) { + List groupItems = entry.getValue(); + String project = entry.getKey(); + + for (int i = 0; i < groupItems.size(); i++) { + CheckItem item = groupItems.get(i); + XWPFTableRow row = table.createRow(); + + if (i == 0) { + XWPFTableCell cell = row.getCell(0); + cell.getCTTc().addNewTcPr().addNewVMerge().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.RESTART); + setCellTextAndStyle(cell, project, false); + } else { + XWPFTableCell cell = row.getCell(0); + cell.getCTTc().addNewTcPr().addNewVMerge().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE); + cell.setText(""); + } + + setCellTextAndStyle(row.getCell(1), item.getCheckItem(), false); + setCellTextAndStyle(row.getCell(2), String.valueOf(item.getItemScore()), false); + setCellTextAndStyle(row.getCell(3), String.valueOf(item.getObtainedScore()), false); + setCellTextAndStyle(row.getCell(4), item.formatScoringExplanation(), false); + } + } + } + + private static void setCellTextAndStyle(XWPFTableCell cell, String text, boolean isHeader) { + cell.getCTTc().addNewTcPr().addNewTcW().setW(BigInteger.valueOf(1800)); + XWPFParagraph p = cell.getParagraphs().get(0); + p.setSpacingAfter(50); + XWPFRun r = p.createRun(); + r.setText(text); + r.setFontFamily("宋体"); + r.setFontSize(10); + if (isHeader) { + r.setBold(true); + } + } + + private static void addTotalAndRemarks(XWPFDocument document, ReportData reportData) { + XWPFParagraph totalPara = document.createParagraph(); + XWPFRun totalRun = totalPara.createRun(); + totalRun.setBold(true); + totalRun.setText("总分:" + reportData.getTotalScore()); + + XWPFParagraph remarksPara = document.createParagraph(); + XWPFRun remarksRun = remarksPara.createRun(); + remarksRun.setText("备注:" + reportData.getRemarks()); + + } + + // ==================== 测试入口 ==================== + public static void main(String[] args) { + try { + ReportData report = new ReportData(); + + report.setReportTitle("数据购买监理服务现场技术核查评分表"); + report.setSiteName("杭州西湖空气质量监测站"); + report.setDistrict("西湖区"); + report.setStreet("龙翔桥"); + report.setOperationUnit("国研信息"); + report.setCheckTime(java.time.LocalDate.now()); + report.setCheckPersonnel("张三、李四"); + //report.setTotalScore(95); + report.setRemarks("注1:《环境空气环境空气气态污染物(SO2、NO2、O3、CO)连续自动监测系统技术要求及检测方法HJ654-2013》\n" + + "*标项一包含的项目,标项二不包含。\n" + + "04质量控制效果中相关质控因子有缺项的,分值按比例分摊。\n"); + + CheckItem item1 = new CheckItem(); + item1.setCheckProject("01 站房环境保障情况"); + item1.setCheckItem("站房清洁与规范管理"); + item1.setItemScore(2); + item1.setObtainedScore(1); + item1.setScoringBasis(List.of( + "1)站房环境脏,有明显灰尘和异味;", + "2)物品摆放不整齐;" + )); + item1.setDeductionRule("一项不合格扣0.5分,扣完为止(评分规则)"); + + CheckItem item2 = new CheckItem(); + item2.setCheckProject("01 站房环境保障情况"); + item2.setCheckItem("仪器运行状态检查"); + item2.setItemScore(3); + item2.setObtainedScore(3); + item2.setScoringBasis(List.of("1)仪器运行正常;")); + item2.setDeductionRule("发现异常未处理扣1分"); + + CheckItem item3 = new CheckItem(); + item3.setCheckProject("02 站房环境保障情况"); + item3.setCheckItem("仪器运行状态检查"); + item3.setItemScore(3); + item3.setObtainedScore(3); + item3.setScoringBasis(List.of("1)仪器运行正常;")); + item3.setDeductionRule("发现异常未处理扣1分"); + + report.setCheckItems(List.of(item1, item2, item3)); + + // 🔢 动态计算总分 + int totalScore = report.getCheckItems().stream() + .mapToInt(CheckItem::getObtainedScore) + .sum(); + int maxScore = report.getCheckItems().stream() + .mapToInt(CheckItem::getItemScore) + .sum(); + + // ✅ 设置为格式化字符串(推荐) + report.setTotalScore(totalScore); + // ✅ 默认导出到桌面 + WordReportExporter.export(report); + + } catch (Exception e) { + e.printStackTrace(); + System.err.println("❌ 导出失败:" + e.getMessage()); + } + } +}