This commit is contained in:
Mrx
2026-03-16 12:32:41 +08:00
parent 09db8d003e
commit 14b2c53eeb
5 changed files with 394 additions and 75 deletions

View File

@@ -98,12 +98,12 @@ func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product,
pdf.SetLineWidth(0.5)
pdf.Line(pageWidth*0.2, pdf.GetY(), pageWidth*0.8, pdf.GetY())
// 产品描述(居中,无单独标题
// 产品描述(居中,宋体小四
if product.Description != "" {
pdf.Ln(10)
desc := pb.textProcessor.HTMLToPlainWithBreaks(product.Description)
desc = pb.textProcessor.CleanText(desc)
pb.fontManager.SetFont(pdf, "", 14)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
_, lineHt = pdf.GetFontSize()
pb.drawRichTextBlock(pdf, desc, pageWidth*0.7, lineHt*1.5, maxContentY, "C", true, chineseFontAvailable)
}
@@ -126,7 +126,7 @@ func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product,
}
}
// AddProductContentPage 添加产品详情页(另起一页,左对齐,段前两空格
// AddProductContentPage 添加产品详情页(另起一页,左对齐,符合 HTML 富文本:段落、加粗、标题
func (pb *PageBuilder) AddProductContentPage(pdf *gofpdf.Fpdf, product *entities.Product, chineseFontAvailable bool) {
if product.Content == "" {
return
@@ -140,12 +140,74 @@ func (pb *PageBuilder) AddProductContentPage(pdf *gofpdf.Fpdf, product *entities
_, 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)
// 按 HTML 富文本解析并绘制(宋体小四):段落、换行、加粗、标题,自动分页且不遮挡 logo
pb.drawHTMLContent(pdf, product.Content, pageWidth*0.9, chineseFontAvailable)
}
// drawHTMLContent 按 HTML 富文本绘制产品详情:段落、换行、加粗、标题;每行前确保在页眉下,避免分页后遮挡 logo
func (pb *PageBuilder) drawHTMLContent(pdf *gofpdf.Fpdf, htmlContent string, contentWidth float64, chineseFontAvailable bool) {
segments := pb.textProcessor.ParseHTMLToSegments(htmlContent)
cleanSegments := make([]HTMLSegment, 0, len(segments))
for _, s := range segments {
t := pb.textProcessor.CleanText(s.Text)
if s.Text != "" {
cleanSegments = append(cleanSegments, HTMLSegment{Text: t, Bold: s.Bold, NewLine: s.NewLine, NewParagraph: s.NewParagraph, HeadingLevel: s.HeadingLevel})
} else {
cleanSegments = append(cleanSegments, s)
}
}
segments = cleanSegments
leftMargin, _, _, _ := pdf.GetMargins()
currentX := leftMargin
firstLineOfBlock := true
for _, seg := range segments {
if seg.NewParagraph {
pdf.Ln(4)
firstLineOfBlock = true
continue
}
if seg.NewLine {
pdf.Ln(1)
continue
}
if seg.Text == "" {
continue
}
// 字体与行高
fontSize := 12.0
style := ""
if seg.Bold {
style = "B"
}
if seg.HeadingLevel == 1 {
fontSize = 18
style = "B"
} else if seg.HeadingLevel == 2 {
fontSize = 16
style = "B"
} else if seg.HeadingLevel == 3 {
fontSize = 14
style = "B"
}
pb.fontManager.SetBodyFont(pdf, style, fontSize)
_, lineHt := pdf.GetFontSize()
lineHeight := lineHt * 1.4
wrapped := pb.safeSplitText(pdf, seg.Text, contentWidth, chineseFontAvailable)
for _, w := range wrapped {
pb.ensureContentBelowHeader(pdf)
x := currentX
if firstLineOfBlock {
x = leftMargin + paragraphIndentMM
}
pdf.SetX(x)
pdf.SetTextColor(0, 0, 0)
pdf.CellFormat(contentWidth, lineHeight, w, "", 1, "L", false, 0, "")
firstLineOfBlock = false
}
}
}
// drawRichTextBlockNoLimit 渲染富文本块,不根据 maxContentY 截断,允许自动分页,适合“产品详情”等必须全部展示的内容
@@ -174,6 +236,7 @@ func (pb *PageBuilder) drawRichTextBlockNoLimit(pdf *gofpdf.Fpdf, text string, c
}
wrapped := pb.safeSplitText(pdf, line, contentWidth, chineseFontAvailable)
for _, w := range wrapped {
pb.ensureContentBelowHeader(pdf)
x := currentX
if align == "L" && firstLineIndent && firstLineOfPara {
x = leftMargin + paragraphIndentMM
@@ -187,7 +250,7 @@ func (pb *PageBuilder) drawRichTextBlockNoLimit(pdf *gofpdf.Fpdf, text string, c
}
// AddDocumentationPages 添加接口文档页面
// 每页的页眉与水印由 SetHeaderFunc 在 AddPage 时自动绘制
// 每页的页眉与水印由 SetHeaderFunc / SetFooterFunc 在 AddPage 时自动绘制
func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
pdf.AddPage()
@@ -204,7 +267,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
// URL使用黑体字体可能包含中文字符
// 先清理URL中的乱码
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
pb.fontManager.SetFont(pdf, "", 10) // 使用黑体
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.SetTextColor(0, 0, 0)
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
@@ -221,6 +284,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
// 请求参数
if doc.RequestParams != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
@@ -234,7 +298,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.RequestParams)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
@@ -251,6 +315,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
// 响应示例
if doc.ResponseExample != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pb.fontManager.SetFont(pdf, "B", 14)
@@ -273,7 +338,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ResponseExample)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
@@ -299,7 +364,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ResponseFields)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
} else {
pb.logger.Warn("返回字段内容为空或只有空白字符")
@@ -311,6 +376,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
// 错误代码
if doc.ErrorCodes != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
@@ -324,7 +390,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ErrorCodes)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
@@ -335,7 +401,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
}
// AddDocumentationPagesWithoutAdditionalInfo 添加接口文档页面(不包含二维码和说明)
// 用于组合包场景,在所有文档渲染完成后统一添加二维码和说明。每页页眉与水印由 SetHeaderFunc 自动绘制。
// 用于组合包场景,在所有文档渲染完成后统一添加二维码和说明。每页页眉与水印由 SetHeaderFunc/SetFooterFunc 自动绘制。
func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
pdf.AddPage()
@@ -352,7 +418,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
// URL使用黑体字体可能包含中文字符
// 先清理URL中的乱码
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
pb.fontManager.SetFont(pdf, "", 10) // 使用黑体
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.SetTextColor(0, 0, 0)
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
@@ -369,6 +435,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
// 请求参数
if doc.RequestParams != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
@@ -382,7 +449,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.RequestParams)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
@@ -399,6 +466,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
// 响应示例
if doc.ResponseExample != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pb.fontManager.SetFont(pdf, "B", 14)
@@ -421,7 +489,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ResponseExample)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
@@ -447,7 +515,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ResponseFields)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
} else {
pb.logger.Warn("返回字段内容为空或只有空白字符")
@@ -459,6 +527,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
// 错误代码
if doc.ErrorCodes != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
@@ -472,7 +541,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ErrorCodes)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
@@ -504,7 +573,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// URL使用黑体字体可能包含中文字符
// 先清理URL中的乱码
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
pb.fontManager.SetFont(pdf, "", 10) // 使用黑体
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.SetTextColor(0, 0, 0)
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
@@ -521,6 +590,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// 请求参数
if doc.RequestParams != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
@@ -534,7 +604,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.RequestParams)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
@@ -551,6 +621,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// 响应示例
if doc.ResponseExample != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pb.fontManager.SetFont(pdf, "B", 14)
@@ -573,7 +644,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ResponseExample)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
@@ -599,7 +670,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ResponseFields)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
} else {
pb.logger.Warn("返回字段内容为空或只有空白字符")
@@ -611,6 +682,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// 错误代码
if doc.ErrorCodes != "" {
pb.ensureContentBelowHeader(pdf)
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
@@ -624,7 +696,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// 如果表格渲染失败,显示为文本
text := pb.textProcessor.CleanText(doc.ErrorCodes)
if strings.TrimSpace(text) != "" {
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
@@ -634,6 +706,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
// addSection 添加章节
func (pb *PageBuilder) addSection(pdf *gofpdf.Fpdf, title, content string, chineseFontAvailable bool) {
pb.ensureContentBelowHeader(pdf)
_, lineHt := pdf.GetFontSize()
pb.fontManager.SetFont(pdf, "B", 14)
pdf.CellFormat(0, lineHt, title+"", "", 1, "L", false, 0, "")
@@ -652,7 +725,7 @@ func (pb *PageBuilder) addSection(pdf *gofpdf.Fpdf, title, content string, chine
jsonContent = formattedJSON
}
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.2, jsonContent, "", "L", false)
} else {
// 按#号标题分割内容,每个标题下的内容单独处理
@@ -726,7 +799,7 @@ func (pb *PageBuilder) processRequestParams(pdf *gofpdf.Fpdf, content string, ch
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
@@ -737,7 +810,7 @@ func (pb *PageBuilder) processRequestParams(pdf *gofpdf.Fpdf, content string, ch
text = pb.textProcessor.CleanText(text)
if strings.TrimSpace(text) != "" {
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
@@ -759,7 +832,7 @@ func (pb *PageBuilder) processResponseExample(pdf *gofpdf.Fpdf, content string,
if err == nil {
jsonContent = formattedJSON
}
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
pdf.Ln(5)
}
@@ -795,7 +868,7 @@ func (pb *PageBuilder) processResponseExample(pdf *gofpdf.Fpdf, content string,
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
@@ -806,7 +879,7 @@ func (pb *PageBuilder) processResponseExample(pdf *gofpdf.Fpdf, content string,
text = pb.textProcessor.CleanText(text)
if strings.TrimSpace(text) != "" {
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
@@ -871,29 +944,32 @@ func (pb *PageBuilder) processSectionContent(pdf *gofpdf.Fpdf, content string, c
// 显示表格前的说明文字
if len(beforeTable) > 0 {
pb.ensureContentBelowHeader(pdf)
beforeText := strings.Join(beforeTable, "\n")
beforeText = pb.textProcessor.StripHTML(beforeText)
beforeText = pb.textProcessor.CleanText(beforeText)
if strings.TrimSpace(beforeText) != "" {
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, beforeText, "", "L", false)
pdf.Ln(3)
}
}
// 渲染表格
pb.ensureContentBelowHeader(pdf)
pb.tableRenderer.RenderTable(pdf, tableData)
// 显示表格后的说明文字
if len(afterTable) > 0 {
pb.ensureContentBelowHeader(pdf)
afterText := strings.Join(afterTable, "\n")
afterText = pb.textProcessor.StripHTML(afterText)
afterText = pb.textProcessor.CleanText(afterText)
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
@@ -901,7 +977,7 @@ func (pb *PageBuilder) processSectionContent(pdf *gofpdf.Fpdf, content string, c
} else {
// 如果不是有效表格显示为文本完整显示markdown内容
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
text := pb.textProcessor.StripHTML(content)
text = pb.textProcessor.CleanText(text) // 清理无效字符,保留中文
// 如果文本不为空,显示它
@@ -913,6 +989,7 @@ func (pb *PageBuilder) processSectionContent(pdf *gofpdf.Fpdf, content string, c
// renderTextWithTitles 渲染包含markdown标题的文本
func (pb *PageBuilder) renderTextWithTitles(pdf *gofpdf.Fpdf, text string, chineseFontAvailable bool, lineHt float64) {
pb.ensureContentBelowHeader(pdf)
lines := strings.Split(text, "\n")
for _, line := range lines {
@@ -946,6 +1023,7 @@ func (pb *PageBuilder) renderTextWithTitles(pdf *gofpdf.Fpdf, text string, chine
}
// 渲染标题
pb.ensureContentBelowHeader(pdf)
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "B", fontSize)
_, titleLineHt := pdf.GetFontSize()
@@ -953,11 +1031,12 @@ func (pb *PageBuilder) renderTextWithTitles(pdf *gofpdf.Fpdf, text string, chine
pdf.Ln(2)
} else if strings.TrimSpace(line) != "" {
// 普通文本行只去除HTML标签保留markdown格式
pb.ensureContentBelowHeader(pdf)
cleanText := pb.textProcessor.StripHTML(line)
cleanText = pb.textProcessor.CleanTextPreservingMarkdown(cleanText)
if strings.TrimSpace(cleanText) != "" {
pdf.SetTextColor(0, 0, 0)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
pdf.MultiCell(0, lineHt*1.3, cleanText, "", "L", false)
}
} else {
@@ -1113,47 +1192,97 @@ func (pb *PageBuilder) getContentPreview(content string, maxLen int) string {
return content[:maxLen] + "..."
}
// drawJSONInCenteredTable 在居中表格中绘制 JSON 文本(表格居中,内容左对齐);空间不足时自动换页避免压住 logo
// wrapJSONLinesToWidth 将 JSON 文本按宽度换行,返回用于绘制的行列表(兼容中文等)
func (pb *PageBuilder) wrapJSONLinesToWidth(pdf *gofpdf.Fpdf, jsonContent string, width float64) []string {
chineseFontAvailable := pb.fontManager != nil && pb.fontManager.IsChineseFontAvailable()
var out []string
for _, line := range strings.Split(jsonContent, "\n") {
line = strings.TrimRight(line, "\r")
if line == "" {
out = append(out, "")
continue
}
wrapped := pb.safeSplitText(pdf, line, width, chineseFontAvailable)
out = append(out, wrapped...)
}
return out
}
// drawJSONInCenteredTable 在居中表格中绘制 JSON 文本(表格居中,内容左对齐);多页时每页独立边框完整包裹当页内容,且不遮挡 logo
func (pb *PageBuilder) drawJSONInCenteredTable(pdf *gofpdf.Fpdf, jsonContent string, lineHt float64) {
jsonContent = strings.TrimSpace(jsonContent)
if jsonContent == "" {
return
}
pb.ensureContentBelowHeader(pdf)
pageWidth, pageHeight := pdf.GetPageSize()
leftMargin, _, rightMargin, bottomMargin := pdf.GetMargins()
usableWidth := pageWidth - leftMargin - rightMargin
// 表格宽度取可用宽度的 85%,居中
tableWidth := usableWidth * 0.85
tableWidth := usableWidth * 0.92
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
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
// 使用 safeSplitText 兼容中文等字符,避免 SplitText panic按行先拆再对每行按宽度换行
allLines := pb.wrapJSONLinesToWidth(pdf, jsonContent, innerWidth)
// 当前页剩余高度不足时先换页,避免表格被 logo 或底部裁掉
currentY := pdf.GetY()
if currentY+totalHeight > pageHeight-bottomMargin {
pdf.AddPage()
currentY = ContentStartYBelowHeader
pdf.SetY(currentY)
// 每页可用高度(从当前 Y 到页底),用于分块
maxH := pageHeight - bottomMargin - pdf.GetY()
linesPerPage := int((maxH - 2*padding) / lineHeight)
if linesPerPage < 1 {
linesPerPage = 1
}
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, "")
chunkStart := 0
for chunkStart < len(allLines) {
pb.ensureContentBelowHeader(pdf)
currentY := pdf.GetY()
// 本页剩余高度不足则换页再从页眉下开始
if currentY < ContentStartYBelowHeader {
currentY = ContentStartYBelowHeader
pdf.SetY(currentY)
}
maxH = pageHeight - bottomMargin - currentY
linesPerPage = int((maxH - 2*padding) / lineHeight)
if linesPerPage < 1 {
pdf.AddPage()
currentY = ContentStartYBelowHeader
pdf.SetY(currentY)
linesPerPage = int((pageHeight - bottomMargin - currentY - 2*padding) / lineHeight)
if linesPerPage < 1 {
linesPerPage = 1
}
}
chunkEnd := chunkStart + linesPerPage
if chunkEnd > len(allLines) {
chunkEnd = len(allLines)
}
chunk := allLines[chunkStart:chunkEnd]
chunkStart = chunkEnd
chunkHeight := float64(len(chunk))*lineHeight + 2*padding
// 若本页放不下整块,先换页
if currentY+chunkHeight > pageHeight-bottomMargin {
pdf.AddPage()
currentY = ContentStartYBelowHeader
pdf.SetY(currentY)
}
startY := currentY
pdf.SetDrawColor(180, 180, 180)
pdf.Rect(startX, startY, tableWidth, chunkHeight, "D")
pdf.SetDrawColor(0, 0, 0)
pdf.SetY(startY + padding)
for _, line := range chunk {
pdf.SetX(startX + padding)
pdf.CellFormat(innerWidth, lineHeight, line, "", 1, "L", false, 0, "")
}
pdf.SetY(startY + chunkHeight)
}
pdf.SetY(startY + totalHeight)
}
// safeSplitText 安全地分割文本避免在没有中文字体时调用SplitText导致panic
@@ -1295,7 +1424,7 @@ func (pb *PageBuilder) addAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.Product
)
pdf.Ln(5)
pb.fontManager.SetFont(pdf, "", 11)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
_, lineHt = pdf.GetFontSize()
// 处理说明文本,按行分割并显示
@@ -1439,7 +1568,7 @@ func (pb *PageBuilder) addQRCodeSection(pdf *gofpdf.Fpdf, doc *entities.ProductD
// 二维码说明文字(简化版,放在二维码之后)
pdf.Ln(10)
pb.fontManager.SetFont(pdf, "", 11)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
_, lineHt = pdf.GetFontSize()
qrCodeExplanation := "使用手机扫描上方二维码可直接跳转到天远API官网https://tianyuanapi.com/),获取更多接口文档和资源。\n\n" +
@@ -1511,7 +1640,7 @@ func (pb *PageBuilder) addQRCodeImage(pdf *gofpdf.Fpdf, content string, chineseF
// 添加二维码说明
pdf.Ln(10)
pb.fontManager.SetFont(pdf, "", 10)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
_, lineHt := pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "官网二维码:", "", 1, "L", false, 0, "")
@@ -1524,7 +1653,7 @@ func (pb *PageBuilder) addQRCodeImage(pdf *gofpdf.Fpdf, content string, chineseF
// 添加二维码下方的说明文字
pdf.SetY(pdf.GetY() + qrSize + 5)
pb.fontManager.SetFont(pdf, "", 9)
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
_, lineHt = pdf.GetFontSize()
qrNote := "使用手机扫描上方二维码可访问官网获取更多详情"
noteWidth := pdf.GetStringWidth(qrNote)