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("