package cn.iocoder.yudao.module.qcreport.service.render; import cn.iocoder.yudao.module.qcreport.engine.Numbers; import cn.iocoder.yudao.module.qcreport.engine.PageSetting; import cn.iocoder.yudao.module.qcreport.engine.PageSizes; import cn.iocoder.yudao.module.qcreport.engine.ResolvedPage; /** * 打印参数:把纸张配置换算成 Chromium 打印 PDF 需要的几何与样式。 *
* 纯函数、不依赖 Spring 与 Playwright,便于单测。 *
* 几何**一律**从 {@link PageSizes#resolve} 取,与 {@code HtmlRenderer} 写进 HTML 的那行 * {@code @page { size: …; margin: …; }} 同一个来源、同一个数字格式化函数 * ({@link Numbers#toString(double)}),因此不存在「PDF 纸面与页面预览对不上」的可能。 * 用 {@code String.format} 会引入 locale 差异并输出 {@code 210.0} 这类与前端不一致的文本。 */ public record PdfPrintOptions(String widthCss, String heightCss, String marginTopCss, String marginRightCss, String marginBottomCss, String marginLeftCss, String footerTemplate, String printCss) { /** * 仅供打印的加固样式,由 Playwright {@code addStyleTag} 注入。 *
* 不能写进渲染引擎的 BASE_CSS:产物 HTML 与前端冻结样例是逐字比对的, * 动它会让存量报告的「重新生成」结果变样。 *
* {@code break-inside: avoid} 绝不能加在 {@code table} 上:那是「整张表不许跨页」, * 长表在一页放不下时会被整体搬到下一页,第 1 页只剩页眉与大片空白(已实际观测到, * 60 行检验项的表就是如此)。长表跨页靠的是 {@code thead} 在每页重复表头 + 行内不断页, * 所以名单里保留 {@code tr/td/th/img}、去掉 {@code table}。 */ public static final String PRINT_CSS = """ thead { display: table-header-group; } tfoot { display: table-footer-group; } tr, td, th, img { break-inside: avoid; page-break-inside: avoid; } body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }"""; /** * 页脚模板。Playwright 的 header/footer 模板不进浏览器默认样式, * 必须自带内联样式与字号,否则是 0 号字看不见。 */ private static final String FOOTER_TEMPLATE = """
* 必须显式给一个非 null 的值。开了 {@code displayHeaderFooter} 却把 headerTemplate * 留空时,Chromium 会用它自己的默认页眉 —— 左上角打印日期({@code 2026/9/18 21:34})、 * 右上角文档标题。那两串东西没人要过,页眉时间还容易被误当成报告出具时间, * 所以这里用一个空 div 把它顶掉,页面上只保留我们自己的页脚页码。 */ public static final String HEADER_TEMPLATE = "
"; public static PdfPrintOptions of(PageSetting page, boolean withPageNumber) { ResolvedPage resolved = PageSizes.resolve(page); return new PdfPrintOptions( mm(resolved.widthMm()), mm(resolved.heightMm()), mm(resolved.marginTopMm()), mm(resolved.marginRightMm()), mm(resolved.marginBottomMm()), mm(resolved.marginLeftMm()), withPageNumber ? FOOTER_TEMPLATE : "", PRINT_CSS); } private static String mm(double value) { return Numbers.toString(value) + "mm"; } }