f
This commit is contained in:
@@ -56,109 +56,142 @@ func NewPageBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
// 封面页底部为价格预留的高度(mm),避免价格被挤到单独一页
|
||||
const firstPagePriceReservedHeight = 18.0
|
||||
|
||||
// ContentStartYBelowHeader 页眉(logo+横线)下方的正文起始 Y(mm),表格等 AddPage 后须设为此值,避免与 logo 重叠(留足顶间距)
|
||||
const ContentStartYBelowHeader = 50.0
|
||||
|
||||
// AddFirstPage 添加第一页(封面页 - 产品功能简述)
|
||||
// 页眉与水印由 SetHeaderFunc 在每页 AddPage 时自动绘制,此处不再重复调用
|
||||
// 自动限制描述/详情高度,保证价格与封面同页,不单独成页
|
||||
func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
|
||||
pdf.AddPage()
|
||||
|
||||
// 添加页眉(logo和文字)
|
||||
pb.addHeader(pdf, chineseFontAvailable)
|
||||
pageWidth, pageHeight := pdf.GetPageSize()
|
||||
_, _, _, bottomMargin := pdf.GetMargins()
|
||||
// 内容区最大 Y:超出则不再绘制,留出底部给价格,避免价格单独一页
|
||||
maxContentY := pageHeight - bottomMargin - firstPagePriceReservedHeight
|
||||
|
||||
// 添加水印
|
||||
pb.addWatermark(pdf, chineseFontAvailable)
|
||||
|
||||
// 封面页布局 - 居中显示
|
||||
pageWidth, _ := pdf.GetPageSize()
|
||||
|
||||
// 标题区域(页面中上部)
|
||||
pdf.SetY(80)
|
||||
// 标题区域(在页眉下方留足间距,避免与 logo 重叠)
|
||||
pdf.SetY(ContentStartYBelowHeader + 6)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pb.fontManager.SetFont(pdf, "B", 32)
|
||||
_, lineHt := pdf.GetFontSize()
|
||||
|
||||
// 清理产品名称中的无效字符
|
||||
cleanName := pb.textProcessor.CleanText(product.Name)
|
||||
pdf.CellFormat(0, lineHt*1.5, cleanName, "", 1, "C", false, 0, "")
|
||||
|
||||
// 添加"接口文档"副标题
|
||||
pdf.Ln(10)
|
||||
pdf.Ln(6)
|
||||
pb.fontManager.SetFont(pdf, "", 18)
|
||||
_, lineHt = pdf.GetFontSize()
|
||||
pdf.CellFormat(0, lineHt, "接口文档", "", 1, "C", false, 0, "")
|
||||
|
||||
// 分隔线
|
||||
pdf.Ln(20)
|
||||
pdf.SetLineWidth(0.5)
|
||||
pdf.Line(pageWidth*0.2, pdf.GetY(), pageWidth*0.8, pdf.GetY())
|
||||
|
||||
// 产品编码(居中)
|
||||
pdf.Ln(30)
|
||||
// 产品编码
|
||||
pdf.Ln(16)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pb.fontManager.SetFont(pdf, "", 14)
|
||||
_, lineHt = pdf.GetFontSize()
|
||||
pdf.CellFormat(0, lineHt, fmt.Sprintf("产品编码:%s", product.Code), "", 1, "C", false, 0, "")
|
||||
|
||||
// 产品描述(居中显示,段落格式)
|
||||
pdf.Ln(12)
|
||||
pdf.SetLineWidth(0.5)
|
||||
pdf.Line(pageWidth*0.2, pdf.GetY(), pageWidth*0.8, pdf.GetY())
|
||||
|
||||
// 产品描述(居中,无单独标题)
|
||||
if product.Description != "" {
|
||||
pdf.Ln(25)
|
||||
desc := pb.textProcessor.StripHTML(product.Description)
|
||||
pdf.Ln(10)
|
||||
desc := pb.textProcessor.HTMLToPlainWithBreaks(product.Description)
|
||||
desc = pb.textProcessor.CleanText(desc)
|
||||
pb.fontManager.SetFont(pdf, "", 14)
|
||||
_, lineHt = pdf.GetFontSize()
|
||||
// 居中对齐的MultiCell(通过计算宽度实现)
|
||||
descWidth := pageWidth * 0.7
|
||||
descLines := pb.safeSplitText(pdf, desc, descWidth, chineseFontAvailable)
|
||||
currentX := (pageWidth - descWidth) / 2
|
||||
for _, line := range descLines {
|
||||
pdf.SetX(currentX)
|
||||
pdf.CellFormat(descWidth, lineHt*1.5, line, "", 1, "C", false, 0, "")
|
||||
}
|
||||
pb.drawRichTextBlock(pdf, desc, pageWidth*0.7, lineHt*1.5, maxContentY, "C", true, chineseFontAvailable)
|
||||
}
|
||||
|
||||
// 产品详情(如果存在)
|
||||
if product.Content != "" {
|
||||
pdf.Ln(20)
|
||||
content := pb.textProcessor.StripHTML(product.Content)
|
||||
content = pb.textProcessor.CleanText(content)
|
||||
pb.fontManager.SetFont(pdf, "", 12)
|
||||
_, lineHt = pdf.GetFontSize()
|
||||
contentWidth := pageWidth * 0.7
|
||||
contentLines := pb.safeSplitText(pdf, content, contentWidth, chineseFontAvailable)
|
||||
currentX := (pageWidth - contentWidth) / 2
|
||||
for _, line := range contentLines {
|
||||
pdf.SetX(currentX)
|
||||
pdf.CellFormat(contentWidth, lineHt*1.4, line, "", 1, "C", false, 0, "")
|
||||
}
|
||||
}
|
||||
|
||||
// 价格信息(右下角,在产品详情之后)
|
||||
// 产品详情已移至单独一页,见 AddProductContentPage
|
||||
if !product.Price.IsZero() {
|
||||
// 获取产品详情结束后的Y坐标,稍微下移显示价格
|
||||
contentEndY := pdf.GetY()
|
||||
pdf.SetY(contentEndY + 5)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pb.fontManager.SetFont(pdf, "", 14)
|
||||
_, priceLineHt := pdf.GetFontSize()
|
||||
reservedZoneY := pageHeight - bottomMargin - firstPagePriceReservedHeight + 6
|
||||
priceY := reservedZoneY
|
||||
if pdf.GetY()+5 > reservedZoneY {
|
||||
priceY = pdf.GetY() + 5
|
||||
}
|
||||
pdf.SetY(priceY)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
priceText := fmt.Sprintf("价格:%s 元", product.Price.String())
|
||||
textWidth := pdf.GetStringWidth(priceText)
|
||||
// 右对齐:从页面宽度减去文本宽度和右边距(15mm)
|
||||
pdf.SetX(pageWidth - textWidth - 15)
|
||||
pdf.CellFormat(textWidth, priceLineHt, priceText, "", 0, "R", false, 0, "")
|
||||
}
|
||||
}
|
||||
|
||||
// AddProductContentPage 添加产品详情页(另起一页,左对齐,段前两空格)
|
||||
func (pb *PageBuilder) AddProductContentPage(pdf *gofpdf.Fpdf, product *entities.Product, chineseFontAvailable bool) {
|
||||
if product.Content == "" {
|
||||
return
|
||||
}
|
||||
pdf.AddPage()
|
||||
pageWidth, _ := pdf.GetPageSize()
|
||||
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pb.fontManager.SetFont(pdf, "B", 14)
|
||||
_, titleHt := pdf.GetFontSize()
|
||||
pdf.CellFormat(0, titleHt, "产品详情", "", 1, "L", false, 0, "")
|
||||
pdf.Ln(6)
|
||||
content := pb.textProcessor.HTMLToPlainWithBreaks(product.Content)
|
||||
content = pb.textProcessor.CleanText(content)
|
||||
pb.fontManager.SetFont(pdf, "", 12)
|
||||
_, lineHt := pdf.GetFontSize()
|
||||
// 产品详情页不做“略写”截断,全部内容都渲染出来,允许 gofpdf 自动分页
|
||||
pb.drawRichTextBlockNoLimit(pdf, content, pageWidth*0.9, lineHt*1.4, "L", true, chineseFontAvailable)
|
||||
}
|
||||
|
||||
// drawRichTextBlockNoLimit 渲染富文本块,不根据 maxContentY 截断,允许自动分页,适合“产品详情”等必须全部展示的内容
|
||||
func (pb *PageBuilder) drawRichTextBlockNoLimit(pdf *gofpdf.Fpdf, text string, contentWidth, lineHeight float64, align string, firstLineIndent bool, chineseFontAvailable bool) {
|
||||
pageWidth, _ := pdf.GetPageSize()
|
||||
leftMargin, _, _, _ := pdf.GetMargins()
|
||||
currentX := (pageWidth - contentWidth) / 2
|
||||
if align == "L" {
|
||||
currentX = leftMargin
|
||||
}
|
||||
paragraphs := strings.Split(text, "\n\n")
|
||||
for pIdx, para := range paragraphs {
|
||||
para = strings.TrimSpace(para)
|
||||
if para == "" {
|
||||
continue
|
||||
}
|
||||
if pIdx > 0 {
|
||||
pdf.Ln(4)
|
||||
}
|
||||
firstLineOfPara := true
|
||||
lines := strings.Split(para, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
wrapped := pb.safeSplitText(pdf, line, contentWidth, chineseFontAvailable)
|
||||
for _, w := range wrapped {
|
||||
x := currentX
|
||||
if align == "L" && firstLineIndent && firstLineOfPara {
|
||||
x = leftMargin + paragraphIndentMM
|
||||
}
|
||||
pdf.SetX(x)
|
||||
pdf.CellFormat(contentWidth, lineHeight, w, "", 1, align, false, 0, "")
|
||||
firstLineOfPara = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddDocumentationPages 添加接口文档页面
|
||||
// 每页的页眉与水印由 SetHeaderFunc 在 AddPage 时自动绘制
|
||||
func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
|
||||
// 创建自定义的AddPage函数,确保每页都有水印
|
||||
addPageWithWatermark := func() {
|
||||
pdf.AddPage()
|
||||
pb.addHeader(pdf, chineseFontAvailable)
|
||||
pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印
|
||||
}
|
||||
pdf.AddPage()
|
||||
|
||||
addPageWithWatermark()
|
||||
|
||||
pdf.SetY(45)
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
pb.fontManager.SetFont(pdf, "B", 18)
|
||||
_, lineHt := pdf.GetFontSize()
|
||||
pdf.CellFormat(0, lineHt, "接口文档", "", 1, "L", false, 0, "")
|
||||
@@ -212,10 +245,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
|
||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||
pb.fontManager.SetFont(pdf, "B", 14)
|
||||
pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "")
|
||||
// JSON中可能包含中文值,使用黑体字体
|
||||
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON(支持中文)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false)
|
||||
pb.drawJSONInCenteredTable(pdf, jsonExample, lineHt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +257,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
|
||||
_, lineHt = pdf.GetFontSize()
|
||||
pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "")
|
||||
|
||||
// 优先尝试提取和格式化JSON
|
||||
// 优先尝试提取和格式化JSON(表格包裹,居中,内容左对齐)
|
||||
jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample)
|
||||
if jsonContent != "" {
|
||||
// 格式化JSON
|
||||
@@ -235,9 +265,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
|
||||
if err == nil {
|
||||
jsonContent = formattedJSON
|
||||
}
|
||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||
pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON(支持中文)
|
||||
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
|
||||
pb.drawJSONInCenteredTable(pdf, jsonContent, lineHt)
|
||||
} else {
|
||||
// 如果没有JSON,尝试使用表格方式处理
|
||||
if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil {
|
||||
@@ -253,8 +281,9 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
|
||||
}
|
||||
}
|
||||
|
||||
// 返回字段说明
|
||||
// 返回字段说明(确保在页眉下方,避免与 logo 重叠)
|
||||
if doc.ResponseFields != "" {
|
||||
pb.ensureContentBelowHeader(pdf)
|
||||
pdf.Ln(8)
|
||||
// 显示标题
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
@@ -306,18 +335,11 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
|
||||
}
|
||||
|
||||
// AddDocumentationPagesWithoutAdditionalInfo 添加接口文档页面(不包含二维码和说明)
|
||||
// 用于组合包场景,在所有文档渲染完成后统一添加二维码和说明
|
||||
// 用于组合包场景,在所有文档渲染完成后统一添加二维码和说明。每页页眉与水印由 SetHeaderFunc 自动绘制。
|
||||
func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
|
||||
// 创建自定义的AddPage函数,确保每页都有水印
|
||||
addPageWithWatermark := func() {
|
||||
pdf.AddPage()
|
||||
pb.addHeader(pdf, chineseFontAvailable)
|
||||
pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印
|
||||
}
|
||||
pdf.AddPage()
|
||||
|
||||
addPageWithWatermark()
|
||||
|
||||
pdf.SetY(45)
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
pb.fontManager.SetFont(pdf, "B", 18)
|
||||
_, lineHt := pdf.GetFontSize()
|
||||
pdf.CellFormat(0, lineHt, "接口文档", "", 1, "L", false, 0, "")
|
||||
@@ -371,10 +393,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||
pb.fontManager.SetFont(pdf, "B", 14)
|
||||
pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "")
|
||||
// JSON中可能包含中文值,使用黑体字体
|
||||
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON(支持中文)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false)
|
||||
pb.drawJSONInCenteredTable(pdf, jsonExample, lineHt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +405,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
||||
_, lineHt = pdf.GetFontSize()
|
||||
pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "")
|
||||
|
||||
// 优先尝试提取和格式化JSON
|
||||
// 优先尝试提取和格式化JSON(表格包裹,居中,内容左对齐)
|
||||
jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample)
|
||||
if jsonContent != "" {
|
||||
// 格式化JSON
|
||||
@@ -394,9 +413,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
||||
if err == nil {
|
||||
jsonContent = formattedJSON
|
||||
}
|
||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||
pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON(支持中文)
|
||||
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
|
||||
pb.drawJSONInCenteredTable(pdf, jsonContent, lineHt)
|
||||
} else {
|
||||
// 如果没有JSON,尝试使用表格方式处理
|
||||
if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil {
|
||||
@@ -412,8 +429,9 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
||||
}
|
||||
}
|
||||
|
||||
// 返回字段说明
|
||||
// 返回字段说明(确保在页眉下方,避免与 logo 重叠)
|
||||
if doc.ResponseFields != "" {
|
||||
pb.ensureContentBelowHeader(pdf)
|
||||
pdf.Ln(8)
|
||||
// 显示标题
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
@@ -463,17 +481,11 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
||||
}
|
||||
|
||||
// AddSubProductDocumentationPages 添加子产品的接口文档页面(用于组合包)
|
||||
// 每页页眉与水印由 SetHeaderFunc 在 AddPage 时自动绘制
|
||||
func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProduct *entities.Product, doc *entities.ProductDocumentation, chineseFontAvailable bool, isLastSubProduct bool) {
|
||||
// 创建自定义的AddPage函数,确保每页都有水印
|
||||
addPageWithWatermark := func() {
|
||||
pdf.AddPage()
|
||||
pb.addHeader(pdf, chineseFontAvailable)
|
||||
pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印
|
||||
}
|
||||
pdf.AddPage()
|
||||
|
||||
addPageWithWatermark()
|
||||
|
||||
pdf.SetY(45)
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
pb.fontManager.SetFont(pdf, "B", 18)
|
||||
_, lineHt := pdf.GetFontSize()
|
||||
|
||||
@@ -533,10 +545,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||
pb.fontManager.SetFont(pdf, "B", 14)
|
||||
pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "")
|
||||
// JSON中可能包含中文值,使用黑体字体
|
||||
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON(支持中文)
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false)
|
||||
pb.drawJSONInCenteredTable(pdf, jsonExample, lineHt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +557,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
||||
_, lineHt = pdf.GetFontSize()
|
||||
pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "")
|
||||
|
||||
// 优先尝试提取和格式化JSON
|
||||
// 优先尝试提取和格式化JSON(表格包裹,居中,内容左对齐)
|
||||
jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample)
|
||||
if jsonContent != "" {
|
||||
// 格式化JSON
|
||||
@@ -556,9 +565,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
||||
if err == nil {
|
||||
jsonContent = formattedJSON
|
||||
}
|
||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||
pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON(支持中文)
|
||||
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
|
||||
pb.drawJSONInCenteredTable(pdf, jsonContent, lineHt)
|
||||
} else {
|
||||
// 如果没有JSON,尝试使用表格方式处理
|
||||
if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil {
|
||||
@@ -574,8 +581,9 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
||||
}
|
||||
}
|
||||
|
||||
// 返回字段说明
|
||||
// 返回字段说明(确保在页眉下方,避免与 logo 重叠)
|
||||
if doc.ResponseFields != "" {
|
||||
pb.ensureContentBelowHeader(pdf)
|
||||
pdf.Ln(8)
|
||||
// 显示标题
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
@@ -979,73 +987,121 @@ func (pb *PageBuilder) addHeader(pdf *gofpdf.Fpdf, chineseFontAvailable bool) {
|
||||
|
||||
// 绘制下横线(优化位置,左边距是15mm)
|
||||
pdf.Line(15, 22, 75, 22)
|
||||
|
||||
// 所有自动分页后的正文统一从页眉下方固定位置开始,避免内容顶到 logo 或水印
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
}
|
||||
|
||||
// addWatermark 添加水印(从左边开始向上倾斜45度,考虑可用区域)
|
||||
// ensureContentBelowHeader 若当前 Y 在页眉区内则下移到正文区,避免与 logo 重叠
|
||||
func (pb *PageBuilder) ensureContentBelowHeader(pdf *gofpdf.Fpdf) {
|
||||
if pdf.GetY() < ContentStartYBelowHeader {
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
}
|
||||
}
|
||||
|
||||
// addWatermark 添加水印:自左下角往右上角倾斜 45°,单条水印居中于页面,样式柔和
|
||||
func (pb *PageBuilder) addWatermark(pdf *gofpdf.Fpdf, chineseFontAvailable bool) {
|
||||
// 如果中文字体不可用,跳过水印(避免显示乱码)
|
||||
if !chineseFontAvailable {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存当前图形状态
|
||||
pdf.TransformBegin()
|
||||
defer pdf.TransformEnd()
|
||||
|
||||
// 获取页面尺寸和边距
|
||||
_, pageHeight := pdf.GetPageSize()
|
||||
pageWidth, pageHeight := pdf.GetPageSize()
|
||||
leftMargin, topMargin, _, bottomMargin := pdf.GetMargins()
|
||||
|
||||
// 计算实际可用区域高度
|
||||
usableHeight := pageHeight - topMargin - bottomMargin
|
||||
usableWidth := pageWidth - leftMargin*2
|
||||
|
||||
// 设置水印样式(使用水印字体,非黑体)
|
||||
fontSize := 45.0
|
||||
|
||||
fontSize := 42.0
|
||||
pb.fontManager.SetWatermarkFont(pdf, "", fontSize)
|
||||
|
||||
// 设置灰色和透明度(加深水印,使其更明显)
|
||||
pdf.SetTextColor(180, 180, 180) // 深一点的灰色
|
||||
pdf.SetAlpha(0.25, "Normal") // 增加透明度,让水印更明显
|
||||
// 加深水印:更深的灰与更高不透明度,保证可见
|
||||
pdf.SetTextColor(150, 150, 150)
|
||||
pdf.SetAlpha(0.32, "Normal")
|
||||
|
||||
// 计算文字宽度
|
||||
textWidth := pdf.GetStringWidth(pb.watermarkText)
|
||||
if textWidth == 0 {
|
||||
// 如果无法获取宽度(字体未注册),使用估算值(中文字符大约每个 fontSize/3 mm)
|
||||
textWidth = float64(len([]rune(pb.watermarkText))) * fontSize / 3.0
|
||||
}
|
||||
|
||||
// 从左边开始,计算起始位置
|
||||
// 起始X:左边距
|
||||
// 起始Y:考虑水印文字长度和旋转后需要的空间
|
||||
startX := leftMargin
|
||||
startY := topMargin + textWidth*0.5 // 为旋转留出空间
|
||||
|
||||
// 移动到起始位置
|
||||
pdf.TransformTranslate(startX, startY)
|
||||
|
||||
// 向上倾斜45度(顺时针旋转45度,即-45度,或逆时针315度)
|
||||
pdf.TransformRotate(-45, 0, 0)
|
||||
|
||||
// 检查文字是否会超出可用区域(旋转后的对角线长度)
|
||||
// 旋转后对角线长度,用于缩放与定位
|
||||
rotatedDiagonal := math.Sqrt(textWidth*textWidth + fontSize*fontSize)
|
||||
if rotatedDiagonal > usableHeight*0.8 {
|
||||
// 如果太大,缩小字体
|
||||
fontSize = fontSize * usableHeight * 0.8 / rotatedDiagonal
|
||||
if rotatedDiagonal > usableHeight*0.75 {
|
||||
fontSize = fontSize * usableHeight * 0.75 / rotatedDiagonal
|
||||
pb.fontManager.SetWatermarkFont(pdf, "", fontSize)
|
||||
textWidth = pdf.GetStringWidth(pb.watermarkText)
|
||||
if textWidth == 0 {
|
||||
textWidth = float64(len([]rune(pb.watermarkText))) * fontSize / 3.0
|
||||
}
|
||||
rotatedDiagonal = math.Sqrt(textWidth*textWidth + fontSize*fontSize)
|
||||
}
|
||||
|
||||
// 从左边开始绘制水印文字
|
||||
// 自左下角往右上角:起点在可用区域左下角,逆时针旋转 +45°
|
||||
startX := leftMargin
|
||||
startY := pageHeight - bottomMargin
|
||||
|
||||
// 沿 +45° 方向居中:对角线在可用区域内居中
|
||||
diagW := rotatedDiagonal * math.Cos(45*math.Pi/180)
|
||||
offsetX := (usableWidth - diagW) * 0.5
|
||||
startX += offsetX
|
||||
startY -= rotatedDiagonal * 0.5
|
||||
|
||||
pdf.TransformTranslate(startX, startY)
|
||||
pdf.TransformRotate(45, 0, 0)
|
||||
|
||||
pdf.SetXY(0, 0)
|
||||
pdf.CellFormat(textWidth, fontSize, pb.watermarkText, "", 0, "L", false, 0, "")
|
||||
|
||||
// 恢复透明度和颜色
|
||||
pdf.SetAlpha(1.0, "Normal")
|
||||
pdf.SetTextColor(0, 0, 0) // 恢复为黑色
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
}
|
||||
|
||||
// 段前缩进宽度(约两字符,mm)
|
||||
const paragraphIndentMM = 7.0
|
||||
|
||||
// drawRichTextBlock 按段落与换行绘制文本块(还原 HTML 换行),超出 maxContentY 截断并显示 …
|
||||
// align: "C" 居中;"L" 左对齐。firstLineIndent 为 true 时每段首行缩进(段前两空格效果)。
|
||||
func (pb *PageBuilder) drawRichTextBlock(pdf *gofpdf.Fpdf, text string, contentWidth, lineHeight float64, maxContentY float64, align string, firstLineIndent bool, chineseFontAvailable bool) {
|
||||
pageWidth, _ := pdf.GetPageSize()
|
||||
leftMargin, _, _, _ := pdf.GetMargins()
|
||||
currentX := (pageWidth - contentWidth) / 2
|
||||
if align == "L" {
|
||||
currentX = leftMargin
|
||||
}
|
||||
paragraphs := strings.Split(text, "\n\n")
|
||||
for pIdx, para := range paragraphs {
|
||||
para = strings.TrimSpace(para)
|
||||
if para == "" {
|
||||
continue
|
||||
}
|
||||
if pIdx > 0 && pdf.GetY()+lineHeight <= maxContentY {
|
||||
pdf.Ln(4)
|
||||
}
|
||||
firstLineOfPara := true
|
||||
lines := strings.Split(para, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
wrapped := pb.safeSplitText(pdf, line, contentWidth, chineseFontAvailable)
|
||||
for _, w := range wrapped {
|
||||
if pdf.GetY()+lineHeight > maxContentY {
|
||||
pdf.SetX(currentX)
|
||||
pdf.CellFormat(contentWidth, lineHeight, "…", "", 1, align, false, 0, "")
|
||||
return
|
||||
}
|
||||
x := currentX
|
||||
if align == "L" && firstLineIndent && firstLineOfPara {
|
||||
x = leftMargin + paragraphIndentMM
|
||||
}
|
||||
pdf.SetX(x)
|
||||
pdf.CellFormat(contentWidth, lineHeight, w, "", 1, align, false, 0, "")
|
||||
firstLineOfPara = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getContentPreview 获取内容预览(用于日志记录)
|
||||
@@ -1057,6 +1113,49 @@ func (pb *PageBuilder) getContentPreview(content string, maxLen int) string {
|
||||
return content[:maxLen] + "..."
|
||||
}
|
||||
|
||||
// drawJSONInCenteredTable 在居中表格中绘制 JSON 文本(表格居中,内容左对齐);空间不足时自动换页避免压住 logo
|
||||
func (pb *PageBuilder) drawJSONInCenteredTable(pdf *gofpdf.Fpdf, jsonContent string, lineHt float64) {
|
||||
jsonContent = strings.TrimSpace(jsonContent)
|
||||
if jsonContent == "" {
|
||||
return
|
||||
}
|
||||
pageWidth, pageHeight := pdf.GetPageSize()
|
||||
leftMargin, _, rightMargin, bottomMargin := pdf.GetMargins()
|
||||
usableWidth := pageWidth - leftMargin - rightMargin
|
||||
// 表格宽度取可用宽度的 85%,居中
|
||||
tableWidth := usableWidth * 0.85
|
||||
startX := (pageWidth - tableWidth) / 2
|
||||
padding := 4.0
|
||||
innerWidth := tableWidth - 2*padding
|
||||
lineHeight := lineHt * 1.3
|
||||
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pb.fontManager.SetFont(pdf, "", 9)
|
||||
lines := pdf.SplitText(jsonContent, innerWidth)
|
||||
totalHeight := float64(len(lines))*lineHeight + 2*padding
|
||||
|
||||
// 当前页剩余高度不足时先换页,避免表格被 logo 或底部裁掉
|
||||
currentY := pdf.GetY()
|
||||
if currentY+totalHeight > pageHeight-bottomMargin {
|
||||
pdf.AddPage()
|
||||
currentY = ContentStartYBelowHeader
|
||||
pdf.SetY(currentY)
|
||||
}
|
||||
|
||||
startY := currentY
|
||||
// 绘制表格边框
|
||||
pdf.SetDrawColor(180, 180, 180)
|
||||
pdf.Rect(startX, startY, tableWidth, totalHeight, "D")
|
||||
pdf.SetDrawColor(0, 0, 0)
|
||||
// 表格内内容左对齐
|
||||
pdf.SetY(startY + padding)
|
||||
for _, line := range lines {
|
||||
pdf.SetX(startX + padding)
|
||||
pdf.CellFormat(innerWidth, lineHeight, line, "", 1, "L", false, 0, "")
|
||||
}
|
||||
pdf.SetY(startY + totalHeight)
|
||||
}
|
||||
|
||||
// safeSplitText 安全地分割文本,避免在没有中文字体时调用SplitText导致panic
|
||||
func (pb *PageBuilder) safeSplitText(pdf *gofpdf.Fpdf, text string, width float64, chineseFontAvailable bool) []string {
|
||||
// 检查文本是否包含中文字符
|
||||
@@ -1167,12 +1266,10 @@ func (pb *PageBuilder) addAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.Product
|
||||
currentY := pdf.GetY()
|
||||
remainingHeight := pageHeight - currentY - bottomMargin
|
||||
|
||||
// 如果剩余空间不足,添加新页
|
||||
// 如果剩余空间不足,添加新页(页眉与水印由 SetHeaderFunc 自动绘制)
|
||||
if remainingHeight < 100 {
|
||||
pdf.AddPage()
|
||||
pb.addHeader(pdf, chineseFontAvailable)
|
||||
pb.addWatermark(pdf, chineseFontAvailable)
|
||||
pdf.SetY(45)
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
}
|
||||
|
||||
// 添加分隔线
|
||||
@@ -1327,9 +1424,7 @@ func (pb *PageBuilder) addQRCodeSection(pdf *gofpdf.Fpdf, doc *entities.ProductD
|
||||
// 检查是否需要换页(为二维码预留空间)
|
||||
if pageHeight-currentY-bottomMargin < 120 {
|
||||
pdf.AddPage()
|
||||
pb.addHeader(pdf, chineseFontAvailable)
|
||||
pb.addWatermark(pdf, chineseFontAvailable)
|
||||
pdf.SetY(45)
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
}
|
||||
|
||||
// 添加二维码标题
|
||||
@@ -1381,9 +1476,7 @@ func (pb *PageBuilder) addQRCodeImage(pdf *gofpdf.Fpdf, content string, chineseF
|
||||
qrSize := 40.0
|
||||
if pageHeight-currentY-bottomMargin < qrSize+20 {
|
||||
pdf.AddPage()
|
||||
pb.addHeader(pdf, chineseFontAvailable)
|
||||
pb.addWatermark(pdf, chineseFontAvailable)
|
||||
pdf.SetY(45)
|
||||
pdf.SetY(ContentStartYBelowHeader)
|
||||
}
|
||||
|
||||
// 生成二维码
|
||||
|
||||
Reference in New Issue
Block a user