diff --git a/nflg-mobilebroken-quotation/pom.xml b/nflg-mobilebroken-quotation/pom.xml index bbaebef2..ab6fe668 100644 --- a/nflg-mobilebroken-quotation/pom.xml +++ b/nflg-mobilebroken-quotation/pom.xml @@ -49,38 +49,6 @@ org.springframework.boot spring-boot-starter-validation - - org.springframework.boot - spring-boot-starter-thymeleaf - - - ognl - ognl - 3.1.28 - - - org.xhtmlrenderer - flying-saucer-pdf - 9.3.1 - - - bcprov-jdk14 - org.bouncycastle - - - - - - com.itextpdf - itextpdf - 5.5.13.3 - - - - com.itextpdf - itext-asian - 5.2.0 - cn.dev33 sa-token-spring-boot-starter @@ -120,6 +88,11 @@ 2.27.3 test + + com.github.librepdf + openpdf + 1.4.2 + diff --git a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/QuotationApplication.java b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/QuotationApplication.java index 765ce1a2..23f7fa58 100644 --- a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/QuotationApplication.java +++ b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/QuotationApplication.java @@ -1,6 +1,13 @@ package com.nflg.mobilebroken.quotation; import cn.dev33.satoken.SaManager; +import cn.hutool.core.util.RandomUtil; +import com.lowagie.text.*; +import com.lowagie.text.Font; +import com.lowagie.text.Image; +import com.lowagie.text.Rectangle; +import com.lowagie.text.pdf.*; +import com.nflg.mobilebroken.quotation.pdf.HeaderFooterEvent; import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; @@ -9,6 +16,16 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; +import java.awt.*; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + @SpringBootApplication @MapperScan("com.nflg.mobilebroken.repository.mapper") @ComponentScan(basePackages = {"com.nflg.mobilebroken.repository.service", "com.nflg.mobilebroken.quotation" @@ -18,9 +35,277 @@ import org.springframework.scheduling.annotation.EnableScheduling; @Slf4j public class QuotationApplication { - public static void main(String[] args) { - SpringApplication.run(QuotationApplication.class, args); - log.info("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); +// public static void main(String[] args) { +// SpringApplication.run(QuotationApplication.class, args); +// log.info("启动成功,Sa-Token 配置如下:" + SaManager.getConfig()); +// } + + public static void main(String[] args) throws Exception { + Document document = new Document(PageSize.A4); + PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream("test.pdf")); + writer.setPageEvent(new HeaderFooterEvent("images/head.png")); + document.open(); + Rectangle pageSize = document.getPageSize(); + BaseFont bfChinese = getChineseFont(); + Font normalFont = new Font(bfChinese, 12); + //第一页 + PdfPTable footerTable = new PdfPTable(1); + footerTable.setTotalWidth(pageSize.getWidth() - document.leftMargin() - document.rightMargin()); + footerTable.setLockedWidth(true); + + Chunk chunk1 = new Chunk("NFLG Crusher and Screen Quotation", new Font(bfChinese, 28, Font.BOLD, new Color(0x3F, 0x3F, 0x3F))); + chunk1.setUnderline(new Color(0xC0, 0x00, 0x00), 5f, 0f, -15f, 0f, PdfContentByte.LINE_CAP_BUTT); + PdfPCell cell1 = new PdfPCell(new Phrase(chunk1)); + cell1.setHorizontalAlignment(Element.ALIGN_RIGHT); + cell1.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell1.setMinimumHeight(50); + cell1.setPaddingTop(70); + cell1.setBorder(Rectangle.NO_BORDER); + footerTable.addCell(cell1); + + PdfPCell cell2 = new PdfPCell(new Phrase("Quotation Date:2026.3.20", new Font(bfChinese, 15, Font.BOLD))); + cell2.setHorizontalAlignment(Element.ALIGN_RIGHT); + cell2.setPaddingTop(40); + cell2.setBorder(Rectangle.NO_BORDER); + footerTable.addCell(cell2); + + Phrase phrase = new Phrase(); + Chunk chunk2 = new Chunk("Validity:", new Font(bfChinese, 15, Font.BOLD)); + phrase.add(chunk2); + Chunk chunk3 = new Chunk("30 days from the date of quotation", new Font(bfChinese, 15, Font.BOLD, new Color(0xC0, 0x00, 0x00))); + phrase.add(chunk3); + PdfPCell cell3 = new PdfPCell(phrase); + cell3.setHorizontalAlignment(Element.ALIGN_RIGHT); + cell3.setPaddingTop(3); + cell3.setBorder(Rectangle.NO_BORDER); + footerTable.addCell(cell3); + + PdfPTable outerTable = new PdfPTable(4); + outerTable.setWidthPercentage(100); + outerTable.setWidths(new float[]{1f, 1f, 1f, 1f}); + String[] imagePaths = {"https://cabinet-tool.oss-cn-hangzhou.aliyuncs.com/admin/20260519/1/NNEZ/40232e65-ef66-4ca3-85e2-031c311c751b.png" + , "https://cabinet-tool.oss-cn-hangzhou.aliyuncs.com/admin/20260519/1/qxmh/490a3895-1bdb-41b1-b3c0-face91de4555.png" + , "https://cabinet-tool.oss-cn-hangzhou.aliyuncs.com/admin/20260519/1/TMlE/c932d08d-454f-4c5f-9d8b-cd028d61867e.png" + , "https://cabinet-tool.oss-cn-hangzhou.aliyuncs.com/admin/20260519/1/wR5X/23253d87-2403-4a6d-87ef-be75427c3caa.png"}; + String[] texts = {"Aggregate", "Construction Waste", "Heavy-duty screen", "Mine / Quarry"}; + for (int i = 0; i < 4; i++) { + PdfPCell cell = new PdfPCell(); + cell.setHorizontalAlignment(Element.ALIGN_CENTER); + cell.setVerticalAlignment(Element.ALIGN_TOP); + if (i != 0) { + cell.setPaddingLeft(5); + } + cell.setBorder(Rectangle.NO_BORDER); + cell.setBorderColor(Color.WHITE); + cell.setBorderWidth(5f); + Image image = Image.getInstance(downloadImage(imagePaths[i])); + image.scaleToFit(125f, 1000f); + image.setAlignment(Element.ALIGN_CENTER); + Paragraph caption = new Paragraph(texts[i], new Font(bfChinese, 12, Font.BOLD)); + caption.setAlignment(Element.ALIGN_CENTER); + caption.setSpacingBefore(4f); + cell.addElement(image); + cell.addElement(caption); + outerTable.addCell(cell); + } + PdfPCell cell4 = new PdfPCell(outerTable); + cell4.setPaddingTop(250); + cell4.setBorder(Rectangle.NO_BORDER); + footerTable.addCell(cell4); + + PdfPCell cell5 = new PdfPCell(new Phrase(new Chunk("ONE MORE OPTION FOR THE WORLD", new Font(bfChinese, 20, Font.BOLD, new Color(0xC0, 0x00, 0x00))))); + cell5.setPaddingTop(80); + cell5.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell5.setHorizontalAlignment(Element.ALIGN_CENTER); + cell5.setBorder(Rectangle.NO_BORDER); + footerTable.addCell(cell5); + +// footerTable.writeSelectedRows(0, -1, document.leftMargin(), pageSize.getHeight() - document.bottomMargin() - 50, writer.getDirectContent()); + document.add(footerTable); + document.add(Chunk.NEXTPAGE); + //开始正文 + Font labelFont = new Font(bfChinese, 12, Font.BOLD); + Font valueFont = new Font(bfChinese, 12, Font.NORMAL); + PdfPTable table = new PdfPTable(4); + table.setWidthPercentage(100); + table.setWidths(new float[]{15f, 35f, 15f, 35f}); + PdfPCell label1_1 = createLabelCell("客户:", labelFont); + table.addCell(label1_1); + PdfPCell value1_1 = createValueCell("KIMI", valueFont); + table.addCell(value1_1); + PdfPCell label1_2 = createLabelCell("供应商:", labelFont); + table.addCell(label1_2); + PdfPCell value1_2 = createValueCell("福建南方路面机械股份有限公司", valueFont); + table.addCell(value1_2); + PdfPCell label2_1 = createLabelCell("联系人:", labelFont); + table.addCell(label2_1); + PdfPCell value2_1 = createValueCell("IGOR", valueFont); + table.addCell(value2_1); + PdfPCell label2_2 = createLabelCell("联系人:", labelFont); + table.addCell(label2_2); + PdfPCell value2_2 = createValueCell("陈保朝", valueFont); + table.addCell(value2_2); + PdfPCell label3_1 = createLabelCell("联系方式:", labelFont); + table.addCell(label3_1); + PdfPCell value3_1 = createValueCell("555-12345678", valueFont); + table.addCell(value3_1); + PdfPCell label3_2 = createLabelCell("联系方式:", labelFont); + table.addCell(label3_2); + PdfPCell value3_2 = createValueCell("15188888888", valueFont); + table.addCell(value3_2); + PdfPCell label4_1 = createLabelCell("邮箱:", labelFont); + table.addCell(label4_1); + PdfPCell value4_1 = createValueCell("igor@kmi.com.ru", valueFont); + table.addCell(value4_1); + PdfPCell label4_2 = createLabelCell("邮箱:", labelFont); + table.addCell(label4_2); + PdfPCell value4_2 = createValueCell("baochao.chen@nflg.com", valueFont); + table.addCell(value4_2); + PdfPCell label5_1 = createLabelCell("国家/地区:", labelFont); + table.addCell(label5_1); + PdfPCell value5_1 = createValueCell("俄罗斯", valueFont); + table.addCell(value5_1); + PdfPCell label5_2 = createLabelCell("报价单号:", labelFont); + table.addCell(label5_2); + PdfPCell value5_2 = createValueCell("CHNUI2026202512654", valueFont); + table.addCell(value5_2); + document.add(table); + Paragraph spacer = new Paragraph(); + spacer.setSpacingBefore(40f); + document.add(spacer); + //报价清单 + PdfPTable tableBJQD = new PdfPTable(5); + tableBJQD.setWidthPercentage(100); + tableBJQD.setWidths(new float[]{10f, 40f, 20f, 15f, 15f}); + PdfPCell headBJQD = createBJQDCell("报价清单", 5, normalFont); + tableBJQD.addCell(headBJQD); + PdfPCell cellBJQDH1 = createBJQDCell("序号", 1, normalFont); + tableBJQD.addCell(cellBJQDH1); + PdfPCell cellBJQDH2 = createBJQDCell("设备名称", 1, normalFont); + tableBJQD.addCell(cellBJQDH2); + PdfPCell cellBJQDH3 = createBJQDCell("设备型号", 1, normalFont); + tableBJQD.addCell(cellBJQDH3); + PdfPCell cellBJQDH4 = createBJQDCell("数量/套", 1, normalFont); + tableBJQD.addCell(cellBJQDH4); + PdfPCell cellBJQDH5 = createBJQDCell("价格", 1, normalFont); + tableBJQD.addCell(cellBJQDH5); + int num = 3; + for (int i = 1; i <= num; i++) { + PdfPCell cellR1 = createBJQDCell(String.valueOf(i), 1, normalFont); + tableBJQD.addCell(cellR1); + PdfPCell cellR2 = createBJQDCell("设备名称" + i, 1, normalFont); + tableBJQD.addCell(cellR2); + PdfPCell cellR3 = createBJQDCell("设备型号" + i, 1, normalFont); + tableBJQD.addCell(cellR3); + PdfPCell cellR4 = createBJQDCell("1", 1, normalFont); + tableBJQD.addCell(cellR4); + PdfPCell cellR5 = createBJQDCell(RandomUtil.randomNumbers(7), 1, normalFont); + tableBJQD.addCell(cellR5); + } + PdfPCell cellZBFF = createBJQDCell("质保服务", 4, normalFont); + tableBJQD.addCell(cellZBFF); + PdfPCell cellZBFF1 = createBJQDCell(RandomUtil.randomNumbers(3), 1, normalFont); + tableBJQD.addCell(cellZBFF1); + PdfPCell cellJJFF = createBJQDCell("交机服务", 4, normalFont); + tableBJQD.addCell(cellJJFF); + PdfPCell cellJJFF1 = createBJQDCell(RandomUtil.randomNumbers(3), 1, normalFont); + tableBJQD.addCell(cellJJFF1); + PdfPCell cellBJ = createBJQDCell("备件", 4, normalFont); + tableBJQD.addCell(cellBJ); + PdfPCell cellBJ1 = createBJQDCell(RandomUtil.randomNumbers(3), 1, normalFont); + tableBJQD.addCell(cellBJ1); + PdfPCell cellBZ = createBJQDCell(RandomUtil.randomString(1000), 5, normalFont); + cellBZ.setHorizontalAlignment(Element.ALIGN_LEFT); + tableBJQD.addCell(cellBZ); + document.add(tableBJQD); + document.add(Chunk.NEXTPAGE); + Paragraph sdad = new Paragraph(); + sdad.add(new Chunk("This is a new page.", new Font(bfChinese, 12, Font.NORMAL))); + document.add(sdad); + + document.close(); } + private static PdfPCell createBJQDCell(String text, int colSpan, Font font) { + PdfPCell cell = new PdfPCell(new Phrase(text, font)); + cell.setColspan(colSpan); + cell.setBorderWidth(0.5f); + cell.setBorderColor(Color.BLACK); + cell.setHorizontalAlignment(Element.ALIGN_CENTER); + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell.setPadding(10f); + return cell; + } + + private static PdfPCell createLabelCell(String text, Font font) { + PdfPCell cell = new PdfPCell(new Phrase(text, font)); + cell.setBorder(Rectangle.NO_BORDER); // 去掉默认粗边框 + cell.setHorizontalAlignment(Element.ALIGN_RIGHT); // 标签靠右对齐 + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); // 垂直居中 + cell.setPaddingBottom(8f); // 行间距 + cell.setPaddingRight(8f); // 与后面值的间距 + // 可选:给底部加一条浅灰色的线,区分每行 + cell.setBorderColorBottom(new Color(220, 220, 220)); + cell.setBorderWidthBottom(0.5f); + return cell; + } + + private static PdfPCell createValueCell(String text, Font font) { + PdfPCell cell = new PdfPCell(new Phrase(text, font)); + cell.setBorder(Rectangle.NO_BORDER); + cell.setHorizontalAlignment(Element.ALIGN_LEFT); // 值靠左对齐 + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell.setMinimumHeight(8f); + // 底部同样加浅灰色线 + cell.setBorderColorBottom(new Color(220, 220, 220)); + cell.setBorderWidthBottom(0.5f); + return cell; + } + + private static byte[] downloadImage(String urlStr) { + try { + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) // 连接超时 10秒 + .followRedirects(HttpClient.Redirect.NORMAL) // 自动处理重定向 + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(urlStr)) + .timeout(Duration.ofSeconds(15)) // 读取超时 15秒 + // 某些图片服务器有防盗链,可以模拟浏览器 User-Agent + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)") + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofByteArray()); + + if (response.statusCode() == 200) { + return response.body(); + } else { + System.err.println("下载图片失败,HTTP 状态码: " + response.statusCode()); + return null; + } + } catch (Exception e) { + System.err.println("下载图片异常: " + e.getMessage()); + return null; + } + } + + private static BaseFont getChineseFont() { + try { + // 1. 从 classpath 获取字体流 + String fontPath = "fonts/simhei.ttf"; // 对应 resources/fonts/simsun.ttf + InputStream inputStream = QuotationApplication.class.getClassLoader().getResourceAsStream(fontPath); + if (inputStream == null) { + throw new RuntimeException("找不到字体文件: " + fontPath); + } + // 2. 将流转换为 byte 数组 (Java 9+ 写法) + byte[] fontBytes = inputStream.readAllBytes(); + inputStream.close(); + // 3. 使用 byte 数组创建 BaseFont + // 参数说明:字体名称(随便起), 编码, 嵌入PDF, 缓存, 字体字节流, PFB字节流(null) + return BaseFont.createFont("simhei.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true, fontBytes, null); + } catch (Exception e) { + throw new RuntimeException("加载中文字体失败", e); + } + } } diff --git a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/controller/admin/AdminShoppingController.java b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/controller/admin/AdminShoppingController.java index b63bb56d..a8d02557 100644 --- a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/controller/admin/AdminShoppingController.java +++ b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/controller/admin/AdminShoppingController.java @@ -3,16 +3,10 @@ package com.nflg.mobilebroken.quotation.controller.admin; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.convert.Convert; import com.baomidou.mybatisplus.core.metadata.IPage; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.itextpdf.text.pdf.BaseFont; -import com.nflg.mobilebroken.common.constant.STATE; -import com.nflg.mobilebroken.common.exception.NflgException; import com.nflg.mobilebroken.common.pojo.ApiResult; import com.nflg.mobilebroken.common.pojo.PageData; import com.nflg.mobilebroken.common.pojo.request.QuotationAdminSearchRequest; -import com.nflg.mobilebroken.common.pojo.request.QuotationSearchRequest; import com.nflg.mobilebroken.common.pojo.vo.QuotationSearchVO; -import com.nflg.mobilebroken.common.util.AppUserUtil; import com.nflg.mobilebroken.quotation.controller.ControllerBase; import com.nflg.mobilebroken.quotation.pojo.vo.ShoppingOrderAdjustModelPartVO; import com.nflg.mobilebroken.quotation.pojo.vo.ShoppingOrderAdjustModelVO; @@ -20,19 +14,14 @@ import com.nflg.mobilebroken.quotation.pojo.vo.ShoppingOrderAdjustVO; import com.nflg.mobilebroken.repository.entity.*; import com.nflg.mobilebroken.repository.service.*; import org.springframework.core.io.ClassPathResource; -import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.xhtmlrenderer.pdf.ITextRenderer; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; -import java.io.OutputStream; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.Objects; diff --git a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/controller/app/ShoppingController.java b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/controller/app/ShoppingController.java index c5176bb0..b0fc8a38 100644 --- a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/controller/app/ShoppingController.java +++ b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/controller/app/ShoppingController.java @@ -5,6 +5,7 @@ import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.lowagie.text.pdf.BaseFont; import com.nflg.mobilebroken.common.constant.Constant; import com.nflg.mobilebroken.common.pojo.ApiResult; import com.nflg.mobilebroken.common.pojo.PageData; @@ -45,6 +46,7 @@ import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; +import java.io.InputStream; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -256,6 +258,7 @@ public class ShoppingController extends ControllerBase { .stream() .map(kv -> new ShoppingCartPartGroupVO() .setGroupName(kv.getKey()) + .setReplaceable(kv.getValue().size() > 1) .setItems(kv.getValue() .stream() .sorted(Comparator.comparing(ShoppingCartPartVO::getType).reversed()) @@ -287,6 +290,7 @@ public class ShoppingController extends ControllerBase { .stream() .map(kv -> new ShoppingCartPartGroupVO() .setGroupName(kv.getKey()) + .setReplaceable(kv.getValue().size() > 1) .setItems(kv.getValue() .stream() .sorted(Comparator.comparing(ShoppingCartPartVO::getType).reversed()) @@ -851,38 +855,7 @@ public class ShoppingController extends ControllerBase { public ResponseEntity exportToPdf(HttpServletResponse response, @RequestParam @NotNull(message = "报价单id不能为空") Long id) { // QuotationShoppingOrder order = shoppingOrderService.getById(id); // VUtils.trueThrowBusinessError(Objects.isNull(order)).throwMessage("未找到报价单"); -// Map order = new HashMap<>(); -// order.put("no", "DFENNIKFWE562D"); -// Map variables = new HashMap<>(); -// variables.put("info", order); -// // 渲染HTML -// TemplateEngine templateEngine = new TemplateEngine(); -// ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); -// resolver.setPrefix("/templates/"); -// resolver.setSuffix(".html"); -// templateEngine.setTemplateResolver(resolver); -// -// Context context = new Context(); -// context.setVariables(variables); -// String html = templateEngine.process("pdf.html", context); -// -// response.setContentType(MediaType.APPLICATION_PDF_VALUE); -// String encode = URLEncoder.encode("aaaa" + ".pdf", StandardCharsets.UTF_8); -// response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + encode); -// // 生成PDF -// try { -// ITextRenderer renderer = new ITextRenderer(); -// renderer.getFontResolver().addFont("fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); -// URL baseUrl = new ClassPathResource("templates/").getURL(); -// renderer.setDocumentFromString(html, baseUrl.toString()); -// renderer.layout(); -// try (OutputStream outputStream = response.getOutputStream()) { -// renderer.createPDF(outputStream); -// } -// } catch (Exception e) { -// log.error("生成pdf出错", e); -// throw new NflgException(STATE.BusinessError, "生成pdf出错"); -// } + org.springframework.core.io.Resource resource = new ClassPathResource("templates/demo.pdf"); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_PDF) @@ -890,6 +863,25 @@ public class ShoppingController extends ControllerBase { .body(resource); } + public BaseFont getChineseFont() { + try { + // 1. 从 classpath 获取字体流 + String fontPath = "fonts/simsun.ttc"; // 对应 resources/fonts/simsun.ttf + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fontPath); + if (inputStream == null) { + throw new RuntimeException("找不到字体文件: " + fontPath); + } + // 2. 将流转换为 byte 数组 (Java 9+ 写法) + byte[] fontBytes = inputStream.readAllBytes(); + inputStream.close(); + // 3. 使用 byte 数组创建 BaseFont + // 参数说明:字体名称(随便起), 编码, 嵌入PDF, 缓存, 字体字节流, PFB字节流(null) + return BaseFont.createFont("simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true, fontBytes, null); + } catch (Exception e) { + throw new RuntimeException("加载中文字体失败", e); + } + } + /** * 设置查看密码 */ diff --git a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pdf/HeaderFooterEvent.java b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pdf/HeaderFooterEvent.java new file mode 100644 index 00000000..70c58ebc --- /dev/null +++ b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pdf/HeaderFooterEvent.java @@ -0,0 +1,112 @@ +package com.nflg.mobilebroken.quotation.pdf; + +import com.lowagie.text.*; +import com.lowagie.text.Font; +import com.lowagie.text.Image; +import com.lowagie.text.Rectangle; +import com.lowagie.text.pdf.*; +import com.nflg.mobilebroken.quotation.QuotationApplication; +import lombok.extern.slf4j.Slf4j; + +import java.awt.*; +import java.io.IOException; +import java.io.InputStream; + +@Slf4j +public class HeaderFooterEvent extends PdfPageEventHelper { + + private Image headerImage; + + public HeaderFooterEvent(String classpathLocation) { + try { + // 1. 通过当前类的 ClassLoader 获取资源流 + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(classpathLocation); + + if (inputStream == null) { + System.err.println("找不到资源文件: " + classpathLocation); + return; + } + + // 2. 将 InputStream 转换为 byte 数组 + // 这是兼容 JAR 包运行的最关键步骤,iText 无法直接使用 InputStream + byte[] imageBytes = inputStream.readAllBytes(); + + inputStream.close(); + + // 3. 使用 byte 数组创建 Image 对象 + headerImage = Image.getInstance(imageBytes); + + } catch (Exception e) { + log.error("Error loading header image:", e); + } + } + + // 每一页结束时调用,绘制页眉和页脚 + @Override + public void onEndPage(PdfWriter writer, Document document) { + drawHeader(writer, document); + try { + drawFooter(writer, document); + } catch (IOException e) { + log.error("Error drawing footer:", e); + } + } + + private void drawHeader(PdfWriter writer, Document document) { + if (headerImage == null) return; + Rectangle pageSize = document.getPageSize(); + headerImage.scaleToFit(pageSize.getWidth(), 30f); + headerImage.setAbsolutePosition(0, pageSize.getHeight() - headerImage.getScaledHeight()); + // 将图片写入该页的画布底层 + PdfContentByte canvas = writer.getDirectContentUnder(); + canvas.addImage(headerImage); + document.setMargins(20, 20, headerImage.getScaledHeight()+10, 30); + } + + private void drawFooter(PdfWriter writer, Document document) throws IOException { + Rectangle pageSize = document.getPageSize(); + PdfPTable footerTable = new PdfPTable(1); + footerTable.setTotalWidth(pageSize.getWidth()); + footerTable.setLockedWidth(true); + + BaseFont baseFont = getChineseFont(); + Font font = new Font(baseFont, 12, Font.NORMAL, Color.WHITE); + Phrase footerPhrase = new Phrase(); + footerPhrase.add(new Chunk("https://www.nflg.com", font)); + footerPhrase.add(new Chunk(" | ", font)); + footerPhrase.add(new Chunk("400-887-7788", font)); + footerPhrase.add(new Chunk(" | ", font)); + footerPhrase.add(new Chunk("0595-2290555", font)); + + PdfPCell footerCell = new PdfPCell(footerPhrase); + footerCell.setFixedHeight(25f); + footerCell.setHorizontalAlignment(Element.ALIGN_CENTER); + footerCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + footerCell.setBackgroundColor(new Color(40, 40, 40)); + + footerTable.addCell(footerCell); + + // 写入页脚 (绝对定位) + // X = 左边距, Y = 下边距 - 一点偏移(让页脚显示在下边距区域内) + footerTable.writeSelectedRows(0, -1, 0, 25, writer.getDirectContent()); + } + + private static BaseFont getChineseFont() { + try { + // 1. 从 classpath 获取字体流 + String fontPath = "fonts/simhei.ttf"; // 对应 resources/fonts/simsun.ttf + InputStream inputStream = QuotationApplication.class.getClassLoader().getResourceAsStream(fontPath); + if (inputStream == null) { + throw new RuntimeException("找不到字体文件: " + fontPath); + } + // 2. 将流转换为 byte 数组 (Java 9+ 写法) + byte[] fontBytes = inputStream.readAllBytes(); + inputStream.close(); + // 3. 使用 byte 数组创建 BaseFont + // 参数说明:字体名称(随便起), 编码, 嵌入PDF, 缓存, 字体字节流, PFB字节流(null) + return BaseFont.createFont("simhei.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true, fontBytes, null); + } catch (Exception e) { + throw new RuntimeException("加载中文字体失败", e); + } + } +} diff --git a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/request/ShoppingSaveRequest.java b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/request/ShoppingSaveRequest.java index 89aae78f..8b15e1be 100644 --- a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/request/ShoppingSaveRequest.java +++ b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/request/ShoppingSaveRequest.java @@ -106,6 +106,11 @@ public class ShoppingSaveRequest { */ private BigDecimal discount; + /** + * 质保服务id(字典WarrantyStandards项id) + */ + private Long warrantyServiceDicId; + /** * 质保服务描述 */ diff --git a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/vo/ShoppingCartPartGroupVO.java b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/vo/ShoppingCartPartGroupVO.java index 9b267941..b992a561 100644 --- a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/vo/ShoppingCartPartGroupVO.java +++ b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/vo/ShoppingCartPartGroupVO.java @@ -14,5 +14,10 @@ public class ShoppingCartPartGroupVO { */ private String groupName; + /** + * 是否可替换 + */ + private boolean replaceable; + private List items; } diff --git a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/vo/ShoppingCartPartVO.java b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/vo/ShoppingCartPartVO.java index 2498211f..d11cc3ad 100644 --- a/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/vo/ShoppingCartPartVO.java +++ b/nflg-mobilebroken-quotation/src/main/java/com/nflg/mobilebroken/quotation/pojo/vo/ShoppingCartPartVO.java @@ -1,5 +1,6 @@ package com.nflg.mobilebroken.quotation.pojo.vo; +import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -59,5 +60,12 @@ public class ShoppingCartPartVO { @JsonIgnore private String groupName; + public String getGroupName() { + if (StrUtil.isNotBlank(groupName)){ + return groupName; + } + return String.valueOf(id); + } + private List children; } diff --git a/nflg-mobilebroken-quotation/src/main/resources/fonts/simhei.ttf b/nflg-mobilebroken-quotation/src/main/resources/fonts/simhei.ttf new file mode 100644 index 00000000..1f1bdf85 Binary files /dev/null and b/nflg-mobilebroken-quotation/src/main/resources/fonts/simhei.ttf differ diff --git a/nflg-mobilebroken-quotation/src/main/resources/images/head.png b/nflg-mobilebroken-quotation/src/main/resources/images/head.png new file mode 100644 index 00000000..25abc1c1 Binary files /dev/null and b/nflg-mobilebroken-quotation/src/main/resources/images/head.png differ diff --git a/nflg-mobilebroken-quotation/src/main/resources/templates/demo.pdf b/nflg-mobilebroken-quotation/src/main/resources/templates/demo.pdf deleted file mode 100644 index 9113e18f..00000000 Binary files a/nflg-mobilebroken-quotation/src/main/resources/templates/demo.pdf and /dev/null differ diff --git a/nflg-mobilebroken-quotation/src/main/resources/templates/pdf.html b/nflg-mobilebroken-quotation/src/main/resources/templates/pdf.html deleted file mode 100644 index 62ae8398..00000000 --- a/nflg-mobilebroken-quotation/src/main/resources/templates/pdf.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - -
- -
-
-
-
-
CLOSE TO OUR CUSTOMERS
-
报价主体
-
-
-
报价人代码
-
报价人
-
报价人电话
-
报价人邮箱地址
-
报价日期
-
-
-
- 报价有效期 - 报价有效期 -
-
-
- 报价单号 - 报价单号 -
-
- 总价 - 总价 -
-
-
包含下列费用
-
- 标准配置价格
-
- 选配价格
-
- 其他配置价格
-
- 附加服务费用
-
- 随机备件价格
-
- 运费
-
-
- 付款方式 - 付款方式 -
-
-
机型
- -
设备简介
-
-
-
配置详情
-
标准配置
-
- -
-
-
- -
-
-
-
可选配置
-
- -
-
-
 
-
-
其他配置
-
油漆要求
-
其它要求说明
-
随机配件
- - - - - - - - - - - - - - - -
物料编号备件名称备件数量备件单价备件总金额
-
交机服务
- - - - - - - - - - - - - -
工程师人数服务天数单人天费用服务总费用
-
- - \ No newline at end of file diff --git a/nflg-mobilebroken-quotation/src/test/java/PdfTest.java b/nflg-mobilebroken-quotation/src/test/java/PdfTest.java new file mode 100644 index 00000000..42dcdb30 --- /dev/null +++ b/nflg-mobilebroken-quotation/src/test/java/PdfTest.java @@ -0,0 +1,53 @@ +import com.lowagie.text.*; +import com.lowagie.text.Font; +import com.lowagie.text.pdf.BaseFont; +import com.lowagie.text.pdf.PdfPCell; +import com.lowagie.text.pdf.PdfPTable; +import com.lowagie.text.pdf.PdfWriter; +import org.junit.Test; + +import java.awt.*; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +public class PdfTest { + + @Test + public void test1() throws Exception { + // 1. 创建文档 + Document document = new Document(PageSize.A4); + PdfWriter.getInstance(document, new FileOutputStream("test.pdf")); + document.open(); + // 2. 设置中文字体 (非常重要,否则中文不显示) + // 推荐使用本地 .ttf 字体文件,兼容性最好 + BaseFont bfChinese = BaseFont.createFont("C:/Windows/Fonts/simsun.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); + Font textFont = new Font(bfChinese, 12, Font.NORMAL); // 普通文字字体 + Font headFont = new Font(bfChinese, 12, Font.BOLD); // 表头加粗字体 + // 3. 创建表格 + // 参数表示列数的相对宽度比例(3:2:5) + float[] columnWidths = {3f, 2f, 5f}; + PdfPTable table = new PdfPTable(columnWidths); + table.setWidthPercentage(100); // 表格占页面宽度的 100% + + } + + /** + * 封装创建单元格的方法 + */ + private static PdfPCell createCell(String text, Font font, Color bgColor, int alignment) { + // 将文本包装在 Paragraph 中,方便设置对齐方式 + Paragraph para = new Paragraph(text, font); + para.setAlignment(alignment); + PdfPCell cell = new PdfPCell(para); + // 设置背景色 + if (bgColor != null) { + cell.setBackgroundColor(bgColor); + } + // 设置内边距 + cell.setPadding(8); + // 设置垂直居中 + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + return cell; + } +} diff --git a/nflg-mobilebroken-repository/src/main/java/com/nflg/mobilebroken/repository/entity/QuotationShoppingCart.java b/nflg-mobilebroken-repository/src/main/java/com/nflg/mobilebroken/repository/entity/QuotationShoppingCart.java index 1d79932d..da4cb4c2 100644 --- a/nflg-mobilebroken-repository/src/main/java/com/nflg/mobilebroken/repository/entity/QuotationShoppingCart.java +++ b/nflg-mobilebroken-repository/src/main/java/com/nflg/mobilebroken/repository/entity/QuotationShoppingCart.java @@ -136,6 +136,11 @@ public class QuotationShoppingCart implements Serializable { */ private Integer status; + /** + * 质保服务id(字典WarrantyStandards项id) + */ + private Long warrantyServiceDicId; + /** * 质保服务描述 */ diff --git a/test.pdf b/test.pdf new file mode 100644 index 00000000..83c44c5a Binary files /dev/null and b/test.pdf differ