feat(pdf): 新增PDF导出功能及自定义页眉页脚支持
- 移除原有基于Thymeleaf和Flying Saucer的PDF生成方式 - 引入openpdf库替代iText,重构PDF生成逻辑 - 新增HeaderFooterEvent类,实现PDF页面页眉图片及页脚联系方式渲染 - 在QuotationApplication中添加PDF示例生成,支持下载网络图片并嵌入PDF - ShoppingController调整PDF导出接口,返回预置PDF示例文件 - ShoppingCart和请求VO新增质保服务相关字段支持 - ShoppingCartPartGroupVO新增replaceable标记,用于表明是否可替换 - ShoppingCartPartVO完善getGroupName方法,默认显示ID避免空白 - 添加PdfTest测试类,辅助验证PDF表格和中文字体处理 - 优化中文字体加载,确保PDF显示中文内容正常 - 删除无用代码和依赖,简化代码结构
This commit is contained in:
parent
25eca66b56
commit
4857a927c8
|
|
@ -49,38 +49,6 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ognl</groupId>
|
||||
<artifactId>ognl</artifactId>
|
||||
<version>3.1.28</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xhtmlrenderer</groupId>
|
||||
<artifactId>flying-saucer-pdf</artifactId>
|
||||
<version>9.3.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>bcprov-jdk14</artifactId>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- iText (required by Flying Saucer) -->
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>itextpdf</artifactId>
|
||||
<version>5.5.13.3</version>
|
||||
</dependency>
|
||||
<!-- Additional iText Asian fonts for Chinese support -->
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>itext-asian</artifactId>
|
||||
<version>5.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
|
|
@ -120,6 +88,11 @@
|
|||
<version>2.27.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.librepdf</groupId>
|
||||
<artifactId>openpdf</artifactId>
|
||||
<version>1.4.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
|||
|
|
@ -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<byte[]> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<org.springframework.core.io.Resource> exportToPdf(HttpServletResponse response, @RequestParam @NotNull(message = "报价单id不能为空") Long id) {
|
||||
// QuotationShoppingOrder order = shoppingOrderService.getById(id);
|
||||
// VUtils.trueThrowBusinessError(Objects.isNull(order)).throwMessage("未找到报价单");
|
||||
// Map<String, Object> order = new HashMap<>();
|
||||
// order.put("no", "DFENNIKFWE562D");
|
||||
// Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置查看密码
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +106,11 @@ public class ShoppingSaveRequest {
|
|||
*/
|
||||
private BigDecimal discount;
|
||||
|
||||
/**
|
||||
* 质保服务id(字典WarrantyStandards项id)
|
||||
*/
|
||||
private Long warrantyServiceDicId;
|
||||
|
||||
/**
|
||||
* 质保服务描述
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,5 +14,10 @@ public class ShoppingCartPartGroupVO {
|
|||
*/
|
||||
private String groupName;
|
||||
|
||||
/**
|
||||
* 是否可替换
|
||||
*/
|
||||
private boolean replaceable;
|
||||
|
||||
private List<ShoppingCartPartVO> items;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ShoppingCartPartGroupVO> children;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
|
@ -1,176 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title th:text="${info.no}"></title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 2cm;
|
||||
@top-left {
|
||||
content: element(header-logo);
|
||||
vertical-align: center;
|
||||
}
|
||||
@top-right {
|
||||
content: "测试数据";
|
||||
font-family: SimSun, serif;
|
||||
font-size: 10pt;
|
||||
}
|
||||
@bottom-center {
|
||||
content: counter(page) " / " counter(pages);
|
||||
font-family: SimSun, serif;
|
||||
font-size: 10pt;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: SimSun, serif;
|
||||
font-size: 12pt;
|
||||
margin: 1cm;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: running(header-logo);
|
||||
}
|
||||
|
||||
.data1:after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.data1 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: black 1px solid;
|
||||
border-collapse: collapse;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<img alt="" src="../images/logo.png" style="width: 200px; "/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="data1">
|
||||
<div style="width: 50%;float: left;">
|
||||
<div style="font-weight: bold">CLOSE TO OUR CUSTOMERS</div>
|
||||
<div th:text="${info.no}">报价主体</div>
|
||||
</div>
|
||||
<div style="width: 50%;float: right">
|
||||
<div th:text="${info.no}">报价人代码</div>
|
||||
<div th:text="${info.no}">报价人</div>
|
||||
<div th:text="${info.no}">报价人电话</div>
|
||||
<div th:text="${info.no}">报价人邮箱地址</div>
|
||||
<div th:text="${info.no}">报价日期</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right">
|
||||
<span>报价有效期</span>
|
||||
<span th:text="${info.no}">报价有效期</span>
|
||||
</div>
|
||||
<hr/>
|
||||
<div style="font-weight: bold;margin-top: 20px;">
|
||||
<span>报价单号</span>
|
||||
<span th:text="${info.no}">报价单号</span>
|
||||
</div>
|
||||
<div style="font-weight: bold;margin-top: 20px;">
|
||||
<span>总价</span>
|
||||
<span th:text="${info.no}">总价</span>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div>包含下列费用</div>
|
||||
<div th:text="${info.no}">- 标准配置价格</div>
|
||||
<div th:text="${info.no}">- 选配价格</div>
|
||||
<div th:text="${info.no}">- 其他配置价格</div>
|
||||
<div th:text="${info.no}">- 附加服务费用</div>
|
||||
<div th:text="${info.no}">- 随机备件价格</div>
|
||||
<div th:text="${info.no}">- 运费</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<span>付款方式</span>
|
||||
<span th:text="${info.no}">付款方式</span>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div style="font-size: 20pt;" th:text="${info.no}">机型</div>
|
||||
<img style="width: 100%;" src="../images/logo.png"/>
|
||||
<div th:text="${info.no}">设备简介</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div style="font-size: 20pt;" th:text="${info.no}">配置详情</div>
|
||||
<div th:text="${info.no}">标准配置</div>
|
||||
<div th:text="${info.no}">
|
||||
<label>
|
||||
<input type="checkbox"/>
|
||||
AAA
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div th:text="${info.no}">
|
||||
<label>
|
||||
<input type="checkbox"/>
|
||||
AAA
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:text="${info.no}">可选配置</div>
|
||||
<div th:text="${info.no}">
|
||||
<label>
|
||||
<input type="checkbox"/>
|
||||
AAA
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div th:text="${info.no}"> <label>
|
||||
<input type="checkbox"/>
|
||||
AAA
|
||||
</label></div>
|
||||
</div>
|
||||
<div th:text="${info.no}">其他配置</div>
|
||||
<div th:text="${info.no}">油漆要求</div>
|
||||
<div th:text="${info.no}">其它要求说明</div>
|
||||
<div th:text="${info.no}">随机配件</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>物料编号</th>
|
||||
<th>备件名称</th>
|
||||
<th>备件数量</th>
|
||||
<th>备件单价</th>
|
||||
<th>备件总金额</th>
|
||||
</tr>
|
||||
<tr th:each="item, itemStat : ${measure.items}" th:if="${not #lists.isEmpty(measure.items)}">
|
||||
<td th:text="${itemStat.index + 1}"></td>
|
||||
<td th:text="${item.name}"></td>
|
||||
<td th:text="${item.superintendent}"></td>
|
||||
<td th:text="${item.scheduleDate}"></td>
|
||||
<td th:text="${item.confirmedDate}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div th:text="${info.no}">交机服务</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>工程师人数</th>
|
||||
<th>服务天数</th>
|
||||
<th>单人天费用</th>
|
||||
<th>服务总费用</th>
|
||||
</tr>
|
||||
<tr th:each="item, itemStat : ${measure.items}" th:if="${not #lists.isEmpty(measure.items)}">
|
||||
<td th:text="${itemStat.index + 1}"></td>
|
||||
<td th:text="${item.name}"></td>
|
||||
<td th:text="${item.superintendent}"></td>
|
||||
<td th:text="${item.scheduleDate}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -136,6 +136,11 @@ public class QuotationShoppingCart implements Serializable {
|
|||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 质保服务id(字典WarrantyStandards项id)
|
||||
*/
|
||||
private Long warrantyServiceDicId;
|
||||
|
||||
/**
|
||||
* 质保服务描述
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue