16 小时以前 9bad721754fe8bbe2e5f459d0706e0fefac569f3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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 需要的几何与样式。
 * <p>
 * 纯函数、不依赖 Spring 与 Playwright,便于单测。
 * <p>
 * 几何**一律**从 {@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} 注入。
     * <p>
     * <b>不能写进渲染引擎的 BASE_CSS</b>:产物 HTML 与前端冻结样例是逐字比对的,
     * 动它会让存量报告的「重新生成」结果变样。
     * <p>
     * <b>{@code break-inside: avoid} 绝不能加在 {@code table} 上</b>:那是「整张表不许跨页」,
     * 长表在一页放不下时会被整体搬到下一页,第 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 = """
            <div style="width:100%;font-size:9px;font-family:'Microsoft YaHei',sans-serif;\
            color:#333;text-align:center;padding:0 10mm;">第 <span class="pageNumber"></span> \
            页 / 共 <span class="totalPages"></span> 页</div>""";
 
    /**
     * 页眉模板:一个空 div。
     * <p>
     * 必须显式给一个<b>非 null</b> 的值。开了 {@code displayHeaderFooter} 却把 headerTemplate
     * 留空时,Chromium 会用它自己的默认页眉 —— 左上角打印日期({@code 2026/9/18 21:34})、
     * 右上角文档标题。那两串东西没人要过,页眉时间还容易被误当成报告出具时间,
     * 所以这里用一个空 div 把它顶掉,页面上只保留我们自己的页脚页码。
     */
    public static final String HEADER_TEMPLATE = "<div></div>";
 
    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";
    }
 
}