如何用go-wkhtmltopdf在Go中生成分页规整的PDF报表?
解决go-wkhtmltopdf生成PDF时页眉错位、表格跨页混乱的问题
核心思路
问题根源在于wkhtmltopdf的自动分页逻辑和自定义页眉/表格布局冲突,要强制每页内容(页眉+20行表格+页脚)作为独立块,禁止跨页拆分,同时禁用工具默认的页眉页脚,改用HTML内联的固定布局。
具体实现步骤
1. 编写带分页控制的HTML模板
重点用CSS控制分页和布局边界:
- 给每页容器设置
page-break-after: always(最后一页取消),确保每页结束后强制分页 - 给表格容器设置
page-break-inside: avoid,防止表格被拆分到两页 - 预留页眉页脚的固定空间,避免内容重叠
示例HTML结构:
<!DOCTYPE html> <html> <head> <style> * { margin: 0; padding: 0; box-sizing: border-box; } /* 每页容器:强制分页,禁止内部拆分 */ .page-container { page-break-after: always; page-break-inside: avoid; width: 100%; height: 100vh; position: relative; } /* 固定页眉:锁定在页顶 */ .page-header { position: absolute; top: 20px; left: 20px; right: 20px; height: 60px; border-bottom: 1px solid #ccc; } .logo { height: 50px; width: auto; } /* 表格容器:预留页眉页脚空间 */ .table-wrapper { padding: 100px 20px 80px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #ccc; padding: 8px; text-align: left; } /* 固定页脚:锁定在页底 */ .page-footer { position: absolute; bottom: 20px; left: 20px; right: 20px; height: 40px; border-top: 1px solid #ccc; text-align: center; font-size: 12px; } /* 最后一页取消强制分页 */ .page-container:last-child { page-break-after: avoid; } </style> </head> <body> {{range .Pages}} <div class="page-container"> <div class="page-header"> <img src="{{.LogoPath}}" class="logo" alt="报表Logo"> <h3>业务数据报表</h3> </div> <div class="table-wrapper"> <table> <thead> <tr> <th>序号</th> <th>业务名称</th> <th>数值</th> </tr> </thead> <tbody> {{range .Rows}} <tr> <td>{{.ID}}</td> <td>{{.Name}}</td> <td>{{.Value}}</td> </tr> {{end}} </tbody> </table> </div> <div class="page-footer"> 第{{.PageNum}}页 / 共{{.TotalPages}}页 </div> </div> {{end}} </body> </html>
2. Go后端处理:拆分数据为每页20行
在Go代码中提前将原始数据按每页20行拆分,生成分页数据结构后渲染模板,避免依赖CSS自动分页的不确定性:
package main import ( "html/template" "os" "github.com/SebastiaanKlippert/go-wkhtmltopdf" ) // 分页数据结构 type PageData struct { LogoPath string PageNum int TotalPages int Rows []RowData } type RowData struct { ID int Name string Value string } func main() { // 模拟原始业务数据 allRows := make([]RowData, 123) for i := 0; i < 123; i++ { allRows[i] = RowData{ ID: i + 1, Name: "业务项" + string(rune(i+1)), Value: "¥" + string(rune(i*100+50)), } } // 拆分数据为每页20行 var pages []PageData pageSize := 20 totalPages := (len(allRows) + pageSize - 1) / pageSize for i := 0; i < totalPages; i++ { start := i * pageSize end := start + pageSize if end > len(allRows) { end = len(allRows) } pages = append(pages, PageData{ LogoPath: "./assets/logo.png", // 替换为实际Logo路径 PageNum: i + 1, TotalPages: totalPages, Rows: allRows[start:end], }) } // 渲染HTML模板 tmpl, err := template.ParseFiles("report.tmpl") if err != nil { panic(err) } htmlFile, err := os.Create("temp_report.html") if err != nil { panic(err) } defer htmlFile.Close() err = tmpl.Execute(htmlFile, map[string]interface{}{"Pages": pages}) if err != nil { panic(err) } // 初始化PDF生成器 pdfg, err := wkhtmltopdf.NewPDFGenerator() if err != nil { panic(err) } // 禁用默认页眉页脚,避免和自定义布局冲突 pdfg.Dpi.Set(300) pdfg.PageSize.Set(wkhtmltopdf.PageSizeA4) pdfg.MarginTop.Set(0) pdfg.MarginBottom.Set(0) pdfg.MarginLeft.Set(0) pdfg.MarginRight.Set(0) // 添加渲染好的HTML页面 page := pdfg.NewPage("file://" + htmlFile.Name()) page.EnableLocalFileAccess.Set(true) // 允许加载本地Logo图片 // 生成并保存PDF err = pdfg.Create() if err != nil { panic(err) } err = pdfg.WriteFile("business_report.pdf") if err != nil { panic(err) } }
3. 关键注意事项
- 必须禁用wkhtmltopdf的默认页眉页脚,否则会和HTML内的固定布局重叠错位
- 后端提前拆分数据为每页20行,比依赖CSS控制行数更可靠,彻底避免跨页行
- 开启
EnableLocalFileAccess确保本地Logo图片能正常加载 - 用
page-break-inside: avoid强制每页容器作为整体,不会被拆分
内容的提问来源于stack exchange,提问作者Sunshine Gaze
相关产品推荐
相关产品推荐

