|
|
@ -0,0 +1,89 @@ |
|
|
|
|
|
package com.fkzy.warn.service; |
|
|
|
|
|
|
|
|
|
|
|
import com.microsoft.playwright.options.Margin; |
|
|
|
|
|
import com.microsoft.playwright.options.WaitUntilState; |
|
|
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
|
|
|
|
|
|
|
|
import com.microsoft.playwright.*; |
|
|
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
|
|
import java.util.regex.Pattern; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* @author zhangjing |
|
|
|
|
|
* @date 2026/01/13 10:33 |
|
|
|
|
|
* @description |
|
|
|
|
|
*/ |
|
|
|
|
|
@Service |
|
|
|
|
|
public class PdfService { |
|
|
|
|
|
// 可选:限制只允许访问你自己的域名(安全) |
|
|
|
|
|
private static final Pattern ALLOWED_URL_PATTERN = |
|
|
|
|
|
Pattern.compile("^https://your-domain\\.com/report/.*"); |
|
|
|
|
|
|
|
|
|
|
|
public byte[] generatePdfFromUrl(String reportUrl) { |
|
|
|
|
|
// 安全校验:防止 SSRF 攻击 |
|
|
|
|
|
// if (!ALLOWED_URL_PATTERN.matcher(reportUrl).matches()) { |
|
|
|
|
|
// throw new IllegalArgumentException("Invalid report URL"); |
|
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
|
|
try (Playwright playwright = Playwright.create()) { |
|
|
|
|
|
Browser browser = playwright.chromium().launch(); |
|
|
|
|
|
Page page = browser.newPage(); |
|
|
|
|
|
|
|
|
|
|
|
// 1. 导航到前端报表页面 |
|
|
|
|
|
page.navigate(reportUrl, new Page.NavigateOptions() |
|
|
|
|
|
.setWaitUntil(WaitUntilState.NETWORKIDLE)); // 等待网络空闲 |
|
|
|
|
|
|
|
|
|
|
|
// 2. 【关键】等待 ECharts 渲染完成 |
|
|
|
|
|
// 方法一:前端在图表加载完成后添加特定 class(推荐) |
|
|
|
|
|
page.waitForSelector("body.report-ready", new Page.WaitForSelectorOptions() |
|
|
|
|
|
.setTimeout(30_000)); // 最多等 30 秒 |
|
|
|
|
|
|
|
|
|
|
|
// 方法二(备选):等待某个图表容器有内容 |
|
|
|
|
|
// page.waitForFunction("() => document.querySelector('#chart').children.length > 0"); |
|
|
|
|
|
|
|
|
|
|
|
// 3. 【可选】注入水印和 Logo(如果前端没做) |
|
|
|
|
|
// 注意:如果前端已包含水印/Logo,此步可跳过 |
|
|
|
|
|
// page.addStyleTag(new Page.AddStyleTagOptions().setContent(""" |
|
|
|
|
|
// .playwright-watermark { |
|
|
|
|
|
// position: fixed; |
|
|
|
|
|
// top: 50%; |
|
|
|
|
|
// left: 50%; |
|
|
|
|
|
// transform: translate(-50%, -50%) rotate(-45deg); |
|
|
|
|
|
// font-size: 80px; |
|
|
|
|
|
// color: rgba(0, 0, 0, 0.08); |
|
|
|
|
|
// pointer-events: none; |
|
|
|
|
|
// z-index: 9999; |
|
|
|
|
|
// white-space: nowrap; |
|
|
|
|
|
// } |
|
|
|
|
|
// """)); |
|
|
|
|
|
page.evaluate("() => { " + |
|
|
|
|
|
"const wm = document.createElement('div');" + |
|
|
|
|
|
"wm.className = 'playwright-watermark';" + |
|
|
|
|
|
"wm.innerText = '机密';" + |
|
|
|
|
|
"document.body.appendChild(wm);" + |
|
|
|
|
|
"}"); |
|
|
|
|
|
|
|
|
|
|
|
// 4. 生成 PDF |
|
|
|
|
|
Margin margin = new Margin(); |
|
|
|
|
|
margin.top="2cm"; |
|
|
|
|
|
margin.bottom="2cm"; |
|
|
|
|
|
margin.left="1.5cm"; |
|
|
|
|
|
margin.right="1.5cm"; |
|
|
|
|
|
byte[] pdfBytes = page.pdf(new Page.PdfOptions() |
|
|
|
|
|
.setFormat("A4") |
|
|
|
|
|
.setPrintBackground(true) |
|
|
|
|
|
.setMargin(margin) |
|
|
|
|
|
.setDisplayHeaderFooter(true) |
|
|
|
|
|
.setHeaderTemplate("<div style='font-size:10px; text-align:center; width:100%;'>公司名称</div>") |
|
|
|
|
|
.setFooterTemplate( |
|
|
|
|
|
"<div style='font-size:9px; width:100%;'>" + |
|
|
|
|
|
"<span style='float:left;'>© 2026 MyCompany</span>" + |
|
|
|
|
|
"<span style='float:right;'>第 <span class='pageNumber'></span> 页 / 共 <span class='totalPages'></span> 页</span>" + |
|
|
|
|
|
"</div>") |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
browser.close(); |
|
|
|
|
|
return pdfBytes; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |