@@ -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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 ] + "..."
}
// d raw JSONInCenteredTable 在居中表格中绘制 JSON 文本(表格居中,内容左对齐);空间不足时自动换页避免压住 logo
// w rap JSONLinesToWidth 将 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 . SetBody Font ( 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
}
s tartY := 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 , "" )
chunkS tart := 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( 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 . SetBody Font ( pdf , "" , BodyFontSizeXiaosi )
_ , lineHt = pdf . GetFontSize ( )
qrNote := "使用手机扫描上方二维码可访问官网获取更多详情"
noteWidth := pdf . GetStringWidth ( qrNote )