f
This commit is contained in:
@@ -30,6 +30,10 @@ func (r *DatabaseTableRenderer) RenderTable(pdf *gofpdf.Fpdf, tableData *TableDa
|
|||||||
r.logger.Warn("表格数据为空,跳过渲染")
|
r.logger.Warn("表格数据为空,跳过渲染")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// 避免表格绘制在页眉区,防止遮挡 logo
|
||||||
|
if pdf.GetY() < ContentStartYBelowHeader {
|
||||||
|
pdf.SetY(ContentStartYBelowHeader)
|
||||||
|
}
|
||||||
|
|
||||||
// 检查表头是否有有效内容
|
// 检查表头是否有有效内容
|
||||||
hasValidHeader := false
|
hasValidHeader := false
|
||||||
@@ -67,8 +71,13 @@ func (r *DatabaseTableRenderer) RenderTable(pdf *gofpdf.Fpdf, tableData *TableDa
|
|||||||
// 即使没有数据行,也渲染表头(单行表格)
|
// 即使没有数据行,也渲染表头(单行表格)
|
||||||
// 但如果没有表头也没有数据,则不渲染
|
// 但如果没有表头也没有数据,则不渲染
|
||||||
|
|
||||||
// 设置字体
|
// 表格线细线(返回字段说明等表格线不要太粗)
|
||||||
r.fontManager.SetFont(pdf, "", 9)
|
savedLineWidth := pdf.GetLineWidth()
|
||||||
|
pdf.SetLineWidth(0.2)
|
||||||
|
defer pdf.SetLineWidth(savedLineWidth)
|
||||||
|
|
||||||
|
// 正文字体:宋体小四 12pt
|
||||||
|
r.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
_, lineHt := pdf.GetFontSize()
|
_, lineHt := pdf.GetFontSize()
|
||||||
|
|
||||||
// 计算页面可用宽度
|
// 计算页面可用宽度
|
||||||
@@ -112,7 +121,7 @@ func (r *DatabaseTableRenderer) RenderTable(pdf *gofpdf.Fpdf, tableData *TableDa
|
|||||||
// 策略:先确保每列最短内容能完整显示(不换行),然后根据内容长度分配剩余空间
|
// 策略:先确保每列最短内容能完整显示(不换行),然后根据内容长度分配剩余空间
|
||||||
func (r *DatabaseTableRenderer) calculateColumnWidths(pdf *gofpdf.Fpdf, tableData *TableData, availableWidth float64) []float64 {
|
func (r *DatabaseTableRenderer) calculateColumnWidths(pdf *gofpdf.Fpdf, tableData *TableData, availableWidth float64) []float64 {
|
||||||
numCols := len(tableData.Headers)
|
numCols := len(tableData.Headers)
|
||||||
r.fontManager.SetFont(pdf, "", 9)
|
r.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
|
|
||||||
// 第一步:找到每列中最短的内容(包括表头),计算其完整显示所需的最小宽度
|
// 第一步:找到每列中最短的内容(包括表头),计算其完整显示所需的最小宽度
|
||||||
colMinWidths := make([]float64, numCols)
|
colMinWidths := make([]float64, numCols)
|
||||||
@@ -363,7 +372,7 @@ func (r *DatabaseTableRenderer) renderHeader(pdf *gofpdf.Fpdf, headers []string,
|
|||||||
// 绘制表头背景和文本
|
// 绘制表头背景和文本
|
||||||
pdf.SetFillColor(74, 144, 226) // 蓝色背景
|
pdf.SetFillColor(74, 144, 226) // 蓝色背景
|
||||||
pdf.SetTextColor(0, 0, 0) // 黑色文字
|
pdf.SetTextColor(0, 0, 0) // 黑色文字
|
||||||
r.fontManager.SetFont(pdf, "B", 9)
|
r.fontManager.SetBodyFont(pdf, "B", BodyFontSizeXiaosi)
|
||||||
|
|
||||||
currentX := 15.0
|
currentX := 15.0
|
||||||
for i, header := range headers {
|
for i, header := range headers {
|
||||||
@@ -396,8 +405,7 @@ func (r *DatabaseTableRenderer) renderHeader(pdf *gofpdf.Fpdf, headers []string,
|
|||||||
pdf.SetXY(currentX+2, textStartY)
|
pdf.SetXY(currentX+2, textStartY)
|
||||||
// 确保颜色为深黑色(在渲染前再次设置,防止被覆盖)
|
// 确保颜色为深黑色(在渲染前再次设置,防止被覆盖)
|
||||||
pdf.SetTextColor(0, 0, 0) // 表头是黑色文字
|
pdf.SetTextColor(0, 0, 0) // 表头是黑色文字
|
||||||
// 设置字体,确保颜色不会变淡
|
r.fontManager.SetBodyFont(pdf, "B", BodyFontSizeXiaosi)
|
||||||
r.fontManager.SetFont(pdf, "B", 9)
|
|
||||||
// 再次确保颜色为深黑色(在渲染前最后一次设置)
|
// 再次确保颜色为深黑色(在渲染前最后一次设置)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
// 使用正常的行高,文本已经垂直居中(减少内边距,给文本更多空间)
|
// 使用正常的行高,文本已经垂直居中(减少内边距,给文本更多空间)
|
||||||
@@ -417,7 +425,7 @@ func (r *DatabaseTableRenderer) renderRows(pdf *gofpdf.Fpdf, rows [][]string, co
|
|||||||
numCols := len(colWidths)
|
numCols := len(colWidths)
|
||||||
pdf.SetFillColor(245, 245, 220) // 米色背景
|
pdf.SetFillColor(245, 245, 220) // 米色背景
|
||||||
pdf.SetTextColor(0, 0, 0) // 深黑色文字,确保清晰
|
pdf.SetTextColor(0, 0, 0) // 深黑色文字,确保清晰
|
||||||
r.fontManager.SetFont(pdf, "", 9)
|
r.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
|
|
||||||
// 获取页面尺寸和边距
|
// 获取页面尺寸和边距
|
||||||
_, pageHeight := pdf.GetPageSize()
|
_, pageHeight := pdf.GetPageSize()
|
||||||
@@ -575,8 +583,7 @@ func (r *DatabaseTableRenderer) renderRows(pdf *gofpdf.Fpdf, rows [][]string, co
|
|||||||
pdf.SetXY(currentX+2, textStartY)
|
pdf.SetXY(currentX+2, textStartY)
|
||||||
// 再次确保颜色为深黑色(防止被其他设置覆盖)
|
// 再次确保颜色为深黑色(防止被其他设置覆盖)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
// 设置字体,确保颜色不会变淡
|
r.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
r.fontManager.SetFont(pdf, "", 9)
|
|
||||||
// 再次确保颜色为深黑色(在渲染前最后一次设置)
|
// 再次确保颜色为深黑色(在渲染前最后一次设置)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
// 安全地渲染文本,使用正常的行高
|
// 安全地渲染文本,使用正常的行高
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ type FontManager struct {
|
|||||||
chineseFontLoaded bool
|
chineseFontLoaded bool
|
||||||
watermarkFontName string
|
watermarkFontName string
|
||||||
watermarkFontLoaded bool
|
watermarkFontLoaded bool
|
||||||
|
bodyFontName string
|
||||||
|
bodyFontLoaded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFontManager 创建字体管理器
|
// NewFontManager 创建字体管理器
|
||||||
@@ -24,6 +26,7 @@ func NewFontManager(logger *zap.Logger) *FontManager {
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
chineseFontName: "ChineseFont",
|
chineseFontName: "ChineseFont",
|
||||||
watermarkFontName: "WatermarkFont",
|
watermarkFontName: "WatermarkFont",
|
||||||
|
bodyFontName: "BodyFont",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +76,21 @@ func (fm *FontManager) LoadWatermarkFont(pdf *gofpdf.Fpdf) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadBodyFont 加载正文用宋体(用于描述、详情、说明、表格文字等)
|
||||||
|
func (fm *FontManager) LoadBodyFont(pdf *gofpdf.Fpdf) bool {
|
||||||
|
if fm.bodyFontLoaded {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
fontPaths := fm.getBodyFontPaths()
|
||||||
|
for _, fontPath := range fontPaths {
|
||||||
|
if fm.tryAddFont(pdf, fontPath, fm.bodyFontName) {
|
||||||
|
fm.bodyFontLoaded = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// tryAddFont 尝试添加字体(统一处理中文字体和水印字体)
|
// tryAddFont 尝试添加字体(统一处理中文字体和水印字体)
|
||||||
func (fm *FontManager) tryAddFont(pdf *gofpdf.Fpdf, fontPath, fontName string) bool {
|
func (fm *FontManager) tryAddFont(pdf *gofpdf.Fpdf, fontPath, fontName string) bool {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -214,6 +232,18 @@ func (fm *FontManager) getWatermarkFontPaths() []string {
|
|||||||
return fm.buildFontPaths(fontNames)
|
return fm.buildFontPaths(fontNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getBodyFontPaths 获取正文宋体路径列表(小四对应 12pt)
|
||||||
|
// 优先使用 resources/pdf/fonts/simsun.ttc(宋体)
|
||||||
|
func (fm *FontManager) getBodyFontPaths() []string {
|
||||||
|
fontNames := []string{
|
||||||
|
// "simsun.ttc", // 宋体(项目内 resources/pdf/fonts)
|
||||||
|
"simsun.ttf",
|
||||||
|
"SimSun.ttf",
|
||||||
|
"WenYuanSerifSC-Bold.ttf", // 文渊宋体风格,备选
|
||||||
|
}
|
||||||
|
return fm.buildFontPaths(fontNames)
|
||||||
|
}
|
||||||
|
|
||||||
// buildFontPaths 构建字体文件路径列表(仅从resources/pdf/fonts加载)
|
// buildFontPaths 构建字体文件路径列表(仅从resources/pdf/fonts加载)
|
||||||
// 返回所有存在的字体文件的绝对路径
|
// 返回所有存在的字体文件的绝对路径
|
||||||
func (fm *FontManager) buildFontPaths(fontNames []string) []string {
|
func (fm *FontManager) buildFontPaths(fontNames []string) []string {
|
||||||
@@ -297,6 +327,28 @@ func (fm *FontManager) SetWatermarkFont(pdf *gofpdf.Fpdf, style string, size flo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BodyFontSizeXiaosi 正文小四字号(约 12pt)
|
||||||
|
const BodyFontSizeXiaosi = 12.0
|
||||||
|
|
||||||
|
// SetBodyFont 设置正文字体(宋体小四:描述、详情、说明、表格文字等)
|
||||||
|
func (fm *FontManager) SetBodyFont(pdf *gofpdf.Fpdf, style string, size float64) {
|
||||||
|
if size <= 0 {
|
||||||
|
size = BodyFontSizeXiaosi
|
||||||
|
}
|
||||||
|
if fm.bodyFontLoaded {
|
||||||
|
pdf.SetFont(fm.bodyFontName, style, size)
|
||||||
|
} else if fm.watermarkFontLoaded {
|
||||||
|
pdf.SetFont(fm.watermarkFontName, style, size)
|
||||||
|
} else {
|
||||||
|
fm.SetFont(pdf, style, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBodyFontAvailable 正文字体(宋体)是否已加载
|
||||||
|
func (fm *FontManager) IsBodyFontAvailable() bool {
|
||||||
|
return fm.bodyFontLoaded || fm.watermarkFontLoaded
|
||||||
|
}
|
||||||
|
|
||||||
// IsChineseFontAvailable 检查中文字体是否可用
|
// IsChineseFontAvailable 检查中文字体是否可用
|
||||||
func (fm *FontManager) IsChineseFontAvailable() bool {
|
func (fm *FontManager) IsChineseFontAvailable() bool {
|
||||||
return fm.chineseFontLoaded
|
return fm.chineseFontLoaded
|
||||||
|
|||||||
@@ -98,12 +98,12 @@ func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product,
|
|||||||
pdf.SetLineWidth(0.5)
|
pdf.SetLineWidth(0.5)
|
||||||
pdf.Line(pageWidth*0.2, pdf.GetY(), pageWidth*0.8, pdf.GetY())
|
pdf.Line(pageWidth*0.2, pdf.GetY(), pageWidth*0.8, pdf.GetY())
|
||||||
|
|
||||||
// 产品描述(居中,无单独标题)
|
// 产品描述(居中,宋体小四)
|
||||||
if product.Description != "" {
|
if product.Description != "" {
|
||||||
pdf.Ln(10)
|
pdf.Ln(10)
|
||||||
desc := pb.textProcessor.HTMLToPlainWithBreaks(product.Description)
|
desc := pb.textProcessor.HTMLToPlainWithBreaks(product.Description)
|
||||||
desc = pb.textProcessor.CleanText(desc)
|
desc = pb.textProcessor.CleanText(desc)
|
||||||
pb.fontManager.SetFont(pdf, "", 14)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
_, lineHt = pdf.GetFontSize()
|
_, lineHt = pdf.GetFontSize()
|
||||||
pb.drawRichTextBlock(pdf, desc, pageWidth*0.7, lineHt*1.5, maxContentY, "C", true, chineseFontAvailable)
|
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) {
|
func (pb *PageBuilder) AddProductContentPage(pdf *gofpdf.Fpdf, product *entities.Product, chineseFontAvailable bool) {
|
||||||
if product.Content == "" {
|
if product.Content == "" {
|
||||||
return
|
return
|
||||||
@@ -140,12 +140,74 @@ func (pb *PageBuilder) AddProductContentPage(pdf *gofpdf.Fpdf, product *entities
|
|||||||
_, titleHt := pdf.GetFontSize()
|
_, titleHt := pdf.GetFontSize()
|
||||||
pdf.CellFormat(0, titleHt, "产品详情", "", 1, "L", false, 0, "")
|
pdf.CellFormat(0, titleHt, "产品详情", "", 1, "L", false, 0, "")
|
||||||
pdf.Ln(6)
|
pdf.Ln(6)
|
||||||
content := pb.textProcessor.HTMLToPlainWithBreaks(product.Content)
|
// 按 HTML 富文本解析并绘制(宋体小四):段落、换行、加粗、标题,自动分页且不遮挡 logo
|
||||||
content = pb.textProcessor.CleanText(content)
|
pb.drawHTMLContent(pdf, product.Content, pageWidth*0.9, chineseFontAvailable)
|
||||||
pb.fontManager.SetFont(pdf, "", 12)
|
}
|
||||||
|
|
||||||
|
// 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()
|
_, lineHt := pdf.GetFontSize()
|
||||||
// 产品详情页不做“略写”截断,全部内容都渲染出来,允许 gofpdf 自动分页
|
lineHeight := lineHt * 1.4
|
||||||
pb.drawRichTextBlockNoLimit(pdf, content, pageWidth*0.9, lineHt*1.4, "L", true, chineseFontAvailable)
|
|
||||||
|
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 截断,允许自动分页,适合“产品详情”等必须全部展示的内容
|
// drawRichTextBlockNoLimit 渲染富文本块,不根据 maxContentY 截断,允许自动分页,适合“产品详情”等必须全部展示的内容
|
||||||
@@ -174,6 +236,7 @@ func (pb *PageBuilder) drawRichTextBlockNoLimit(pdf *gofpdf.Fpdf, text string, c
|
|||||||
}
|
}
|
||||||
wrapped := pb.safeSplitText(pdf, line, contentWidth, chineseFontAvailable)
|
wrapped := pb.safeSplitText(pdf, line, contentWidth, chineseFontAvailable)
|
||||||
for _, w := range wrapped {
|
for _, w := range wrapped {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
x := currentX
|
x := currentX
|
||||||
if align == "L" && firstLineIndent && firstLineOfPara {
|
if align == "L" && firstLineIndent && firstLineOfPara {
|
||||||
x = leftMargin + paragraphIndentMM
|
x = leftMargin + paragraphIndentMM
|
||||||
@@ -187,7 +250,7 @@ func (pb *PageBuilder) drawRichTextBlockNoLimit(pdf *gofpdf.Fpdf, text string, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddDocumentationPages 添加接口文档页面
|
// AddDocumentationPages 添加接口文档页面
|
||||||
// 每页的页眉与水印由 SetHeaderFunc 在 AddPage 时自动绘制
|
// 每页的页眉与水印由 SetHeaderFunc / SetFooterFunc 在 AddPage 时自动绘制
|
||||||
func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
|
func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
|
||||||
pdf.AddPage()
|
pdf.AddPage()
|
||||||
|
|
||||||
@@ -204,7 +267,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
|
|||||||
// URL使用黑体字体(可能包含中文字符)
|
// URL使用黑体字体(可能包含中文字符)
|
||||||
// 先清理URL中的乱码
|
// 先清理URL中的乱码
|
||||||
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
|
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
|
||||||
pb.fontManager.SetFont(pdf, "", 10) // 使用黑体
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
|
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 != "" {
|
if doc.RequestParams != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
// 显示标题
|
// 显示标题
|
||||||
pdf.SetTextColor(0, 0, 0)
|
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)
|
text := pb.textProcessor.CleanText(doc.RequestParams)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
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 != "" {
|
if doc.ResponseExample != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||||
pb.fontManager.SetFont(pdf, "B", 14)
|
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)
|
text := pb.textProcessor.CleanText(doc.ResponseExample)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
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)
|
text := pb.textProcessor.CleanText(doc.ResponseFields)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
||||||
} else {
|
} else {
|
||||||
pb.logger.Warn("返回字段内容为空或只有空白字符")
|
pb.logger.Warn("返回字段内容为空或只有空白字符")
|
||||||
@@ -311,6 +376,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
|
|||||||
|
|
||||||
// 错误代码
|
// 错误代码
|
||||||
if doc.ErrorCodes != "" {
|
if doc.ErrorCodes != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
// 显示标题
|
// 显示标题
|
||||||
pdf.SetTextColor(0, 0, 0)
|
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)
|
text := pb.textProcessor.CleanText(doc.ErrorCodes)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +401,7 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddDocumentationPagesWithoutAdditionalInfo 添加接口文档页面(不包含二维码和说明)
|
// AddDocumentationPagesWithoutAdditionalInfo 添加接口文档页面(不包含二维码和说明)
|
||||||
// 用于组合包场景,在所有文档渲染完成后统一添加二维码和说明。每页页眉与水印由 SetHeaderFunc 自动绘制。
|
// 用于组合包场景,在所有文档渲染完成后统一添加二维码和说明。每页页眉与水印由 SetHeaderFunc/SetFooterFunc 自动绘制。
|
||||||
func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
|
func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
|
||||||
pdf.AddPage()
|
pdf.AddPage()
|
||||||
|
|
||||||
@@ -352,7 +418,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
|||||||
// URL使用黑体字体(可能包含中文字符)
|
// URL使用黑体字体(可能包含中文字符)
|
||||||
// 先清理URL中的乱码
|
// 先清理URL中的乱码
|
||||||
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
|
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
|
||||||
pb.fontManager.SetFont(pdf, "", 10) // 使用黑体
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
|
||||||
|
|
||||||
@@ -369,6 +435,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
|||||||
|
|
||||||
// 请求参数
|
// 请求参数
|
||||||
if doc.RequestParams != "" {
|
if doc.RequestParams != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
// 显示标题
|
// 显示标题
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
@@ -382,7 +449,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
|||||||
// 如果表格渲染失败,显示为文本
|
// 如果表格渲染失败,显示为文本
|
||||||
text := pb.textProcessor.CleanText(doc.RequestParams)
|
text := pb.textProcessor.CleanText(doc.RequestParams)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,6 +466,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
|||||||
|
|
||||||
// 响应示例
|
// 响应示例
|
||||||
if doc.ResponseExample != "" {
|
if doc.ResponseExample != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||||
pb.fontManager.SetFont(pdf, "B", 14)
|
pb.fontManager.SetFont(pdf, "B", 14)
|
||||||
@@ -421,7 +489,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
|||||||
// 如果表格渲染失败,显示为文本
|
// 如果表格渲染失败,显示为文本
|
||||||
text := pb.textProcessor.CleanText(doc.ResponseExample)
|
text := pb.textProcessor.CleanText(doc.ResponseExample)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
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)
|
text := pb.textProcessor.CleanText(doc.ResponseFields)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
||||||
} else {
|
} else {
|
||||||
pb.logger.Warn("返回字段内容为空或只有空白字符")
|
pb.logger.Warn("返回字段内容为空或只有空白字符")
|
||||||
@@ -459,6 +527,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
|||||||
|
|
||||||
// 错误代码
|
// 错误代码
|
||||||
if doc.ErrorCodes != "" {
|
if doc.ErrorCodes != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
// 显示标题
|
// 显示标题
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
@@ -472,7 +541,7 @@ func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fp
|
|||||||
// 如果表格渲染失败,显示为文本
|
// 如果表格渲染失败,显示为文本
|
||||||
text := pb.textProcessor.CleanText(doc.ErrorCodes)
|
text := pb.textProcessor.CleanText(doc.ErrorCodes)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,7 +573,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
|||||||
// URL使用黑体字体(可能包含中文字符)
|
// URL使用黑体字体(可能包含中文字符)
|
||||||
// 先清理URL中的乱码
|
// 先清理URL中的乱码
|
||||||
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
|
cleanURL := pb.textProcessor.CleanText(doc.RequestURL)
|
||||||
pb.fontManager.SetFont(pdf, "", 10) // 使用黑体
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
|
||||||
|
|
||||||
@@ -521,6 +590,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
|||||||
|
|
||||||
// 请求参数
|
// 请求参数
|
||||||
if doc.RequestParams != "" {
|
if doc.RequestParams != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
// 显示标题
|
// 显示标题
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
@@ -534,7 +604,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
|||||||
// 如果表格渲染失败,显示为文本
|
// 如果表格渲染失败,显示为文本
|
||||||
text := pb.textProcessor.CleanText(doc.RequestParams)
|
text := pb.textProcessor.CleanText(doc.RequestParams)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -551,6 +621,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
|||||||
|
|
||||||
// 响应示例
|
// 响应示例
|
||||||
if doc.ResponseExample != "" {
|
if doc.ResponseExample != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||||
pb.fontManager.SetFont(pdf, "B", 14)
|
pb.fontManager.SetFont(pdf, "B", 14)
|
||||||
@@ -573,7 +644,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
|||||||
// 如果表格渲染失败,显示为文本
|
// 如果表格渲染失败,显示为文本
|
||||||
text := pb.textProcessor.CleanText(doc.ResponseExample)
|
text := pb.textProcessor.CleanText(doc.ResponseExample)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
pdf.SetTextColor(0, 0, 0) // 确保深黑色
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
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)
|
text := pb.textProcessor.CleanText(doc.ResponseFields)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
||||||
} else {
|
} else {
|
||||||
pb.logger.Warn("返回字段内容为空或只有空白字符")
|
pb.logger.Warn("返回字段内容为空或只有空白字符")
|
||||||
@@ -611,6 +682,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
|||||||
|
|
||||||
// 错误代码
|
// 错误代码
|
||||||
if doc.ErrorCodes != "" {
|
if doc.ErrorCodes != "" {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pdf.Ln(8)
|
pdf.Ln(8)
|
||||||
// 显示标题
|
// 显示标题
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
@@ -624,7 +696,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
|||||||
// 如果表格渲染失败,显示为文本
|
// 如果表格渲染失败,显示为文本
|
||||||
text := pb.textProcessor.CleanText(doc.ErrorCodes)
|
text := pb.textProcessor.CleanText(doc.ErrorCodes)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,6 +706,7 @@ func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProd
|
|||||||
|
|
||||||
// addSection 添加章节
|
// addSection 添加章节
|
||||||
func (pb *PageBuilder) addSection(pdf *gofpdf.Fpdf, title, content string, chineseFontAvailable bool) {
|
func (pb *PageBuilder) addSection(pdf *gofpdf.Fpdf, title, content string, chineseFontAvailable bool) {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
_, lineHt := pdf.GetFontSize()
|
_, lineHt := pdf.GetFontSize()
|
||||||
pb.fontManager.SetFont(pdf, "B", 14)
|
pb.fontManager.SetFont(pdf, "B", 14)
|
||||||
pdf.CellFormat(0, lineHt, title+":", "", 1, "L", false, 0, "")
|
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
|
jsonContent = formattedJSON
|
||||||
}
|
}
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.2, jsonContent, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.2, jsonContent, "", "L", false)
|
||||||
} else {
|
} else {
|
||||||
// 按#号标题分割内容,每个标题下的内容单独处理
|
// 按#号标题分割内容,每个标题下的内容单独处理
|
||||||
@@ -726,7 +799,7 @@ func (pb *PageBuilder) processRequestParams(pdf *gofpdf.Fpdf, content string, ch
|
|||||||
if strings.TrimSpace(afterText) != "" {
|
if strings.TrimSpace(afterText) != "" {
|
||||||
pdf.Ln(3)
|
pdf.Ln(3)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
|
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)
|
text = pb.textProcessor.CleanText(text)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
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 {
|
if err == nil {
|
||||||
jsonContent = formattedJSON
|
jsonContent = formattedJSON
|
||||||
}
|
}
|
||||||
pb.fontManager.SetFont(pdf, "", 9) // 使用黑体
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
|
||||||
pdf.Ln(5)
|
pdf.Ln(5)
|
||||||
}
|
}
|
||||||
@@ -795,7 +868,7 @@ func (pb *PageBuilder) processResponseExample(pdf *gofpdf.Fpdf, content string,
|
|||||||
if strings.TrimSpace(afterText) != "" {
|
if strings.TrimSpace(afterText) != "" {
|
||||||
pdf.Ln(3)
|
pdf.Ln(3)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
|
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)
|
text = pb.textProcessor.CleanText(text)
|
||||||
if strings.TrimSpace(text) != "" {
|
if strings.TrimSpace(text) != "" {
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
|
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 {
|
if len(beforeTable) > 0 {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
beforeText := strings.Join(beforeTable, "\n")
|
beforeText := strings.Join(beforeTable, "\n")
|
||||||
beforeText = pb.textProcessor.StripHTML(beforeText)
|
beforeText = pb.textProcessor.StripHTML(beforeText)
|
||||||
beforeText = pb.textProcessor.CleanText(beforeText)
|
beforeText = pb.textProcessor.CleanText(beforeText)
|
||||||
if strings.TrimSpace(beforeText) != "" {
|
if strings.TrimSpace(beforeText) != "" {
|
||||||
pdf.SetTextColor(0, 0, 0)
|
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.MultiCell(0, lineHt*1.3, beforeText, "", "L", false)
|
||||||
pdf.Ln(3)
|
pdf.Ln(3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染表格
|
// 渲染表格
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
pb.tableRenderer.RenderTable(pdf, tableData)
|
pb.tableRenderer.RenderTable(pdf, tableData)
|
||||||
|
|
||||||
// 显示表格后的说明文字
|
// 显示表格后的说明文字
|
||||||
if len(afterTable) > 0 {
|
if len(afterTable) > 0 {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
afterText := strings.Join(afterTable, "\n")
|
afterText := strings.Join(afterTable, "\n")
|
||||||
afterText = pb.textProcessor.StripHTML(afterText)
|
afterText = pb.textProcessor.StripHTML(afterText)
|
||||||
afterText = pb.textProcessor.CleanText(afterText)
|
afterText = pb.textProcessor.CleanText(afterText)
|
||||||
if strings.TrimSpace(afterText) != "" {
|
if strings.TrimSpace(afterText) != "" {
|
||||||
pdf.Ln(3)
|
pdf.Ln(3)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -901,7 +977,7 @@ func (pb *PageBuilder) processSectionContent(pdf *gofpdf.Fpdf, content string, c
|
|||||||
} else {
|
} else {
|
||||||
// 如果不是有效表格,显示为文本(完整显示markdown内容)
|
// 如果不是有效表格,显示为文本(完整显示markdown内容)
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
text := pb.textProcessor.StripHTML(content)
|
text := pb.textProcessor.StripHTML(content)
|
||||||
text = pb.textProcessor.CleanText(text) // 清理无效字符,保留中文
|
text = pb.textProcessor.CleanText(text) // 清理无效字符,保留中文
|
||||||
// 如果文本不为空,显示它
|
// 如果文本不为空,显示它
|
||||||
@@ -913,6 +989,7 @@ func (pb *PageBuilder) processSectionContent(pdf *gofpdf.Fpdf, content string, c
|
|||||||
|
|
||||||
// renderTextWithTitles 渲染包含markdown标题的文本
|
// renderTextWithTitles 渲染包含markdown标题的文本
|
||||||
func (pb *PageBuilder) renderTextWithTitles(pdf *gofpdf.Fpdf, text string, chineseFontAvailable bool, lineHt float64) {
|
func (pb *PageBuilder) renderTextWithTitles(pdf *gofpdf.Fpdf, text string, chineseFontAvailable bool, lineHt float64) {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
lines := strings.Split(text, "\n")
|
lines := strings.Split(text, "\n")
|
||||||
|
|
||||||
for _, line := range lines {
|
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)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "B", fontSize)
|
pb.fontManager.SetFont(pdf, "B", fontSize)
|
||||||
_, titleLineHt := pdf.GetFontSize()
|
_, titleLineHt := pdf.GetFontSize()
|
||||||
@@ -953,11 +1031,12 @@ func (pb *PageBuilder) renderTextWithTitles(pdf *gofpdf.Fpdf, text string, chine
|
|||||||
pdf.Ln(2)
|
pdf.Ln(2)
|
||||||
} else if strings.TrimSpace(line) != "" {
|
} else if strings.TrimSpace(line) != "" {
|
||||||
// 普通文本行(只去除HTML标签,保留markdown格式)
|
// 普通文本行(只去除HTML标签,保留markdown格式)
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
cleanText := pb.textProcessor.StripHTML(line)
|
cleanText := pb.textProcessor.StripHTML(line)
|
||||||
cleanText = pb.textProcessor.CleanTextPreservingMarkdown(cleanText)
|
cleanText = pb.textProcessor.CleanTextPreservingMarkdown(cleanText)
|
||||||
if strings.TrimSpace(cleanText) != "" {
|
if strings.TrimSpace(cleanText) != "" {
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
pdf.MultiCell(0, lineHt*1.3, cleanText, "", "L", false)
|
pdf.MultiCell(0, lineHt*1.3, cleanText, "", "L", false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1113,47 +1192,97 @@ func (pb *PageBuilder) getContentPreview(content string, maxLen int) string {
|
|||||||
return content[:maxLen] + "..."
|
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) {
|
func (pb *PageBuilder) drawJSONInCenteredTable(pdf *gofpdf.Fpdf, jsonContent string, lineHt float64) {
|
||||||
jsonContent = strings.TrimSpace(jsonContent)
|
jsonContent = strings.TrimSpace(jsonContent)
|
||||||
if jsonContent == "" {
|
if jsonContent == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
|
|
||||||
pageWidth, pageHeight := pdf.GetPageSize()
|
pageWidth, pageHeight := pdf.GetPageSize()
|
||||||
leftMargin, _, rightMargin, bottomMargin := pdf.GetMargins()
|
leftMargin, _, rightMargin, bottomMargin := pdf.GetMargins()
|
||||||
usableWidth := pageWidth - leftMargin - rightMargin
|
usableWidth := pageWidth - leftMargin - rightMargin
|
||||||
// 表格宽度取可用宽度的 85%,居中
|
tableWidth := usableWidth * 0.92
|
||||||
tableWidth := usableWidth * 0.85
|
|
||||||
startX := (pageWidth - tableWidth) / 2
|
startX := (pageWidth - tableWidth) / 2
|
||||||
padding := 4.0
|
padding := 4.0
|
||||||
innerWidth := tableWidth - 2*padding
|
innerWidth := tableWidth - 2*padding
|
||||||
lineHeight := lineHt * 1.3
|
lineHeight := lineHt * 1.3
|
||||||
|
|
||||||
pdf.SetTextColor(0, 0, 0)
|
pdf.SetTextColor(0, 0, 0)
|
||||||
pb.fontManager.SetFont(pdf, "", 9)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
lines := pdf.SplitText(jsonContent, innerWidth)
|
// 使用 safeSplitText 兼容中文等字符,避免 SplitText panic;按行先拆再对每行按宽度换行
|
||||||
totalHeight := float64(len(lines))*lineHeight + 2*padding
|
allLines := pb.wrapJSONLinesToWidth(pdf, jsonContent, innerWidth)
|
||||||
|
|
||||||
// 当前页剩余高度不足时先换页,避免表格被 logo 或底部裁掉
|
// 每页可用高度(从当前 Y 到页底),用于分块
|
||||||
|
maxH := pageHeight - bottomMargin - pdf.GetY()
|
||||||
|
linesPerPage := int((maxH - 2*padding) / lineHeight)
|
||||||
|
if linesPerPage < 1 {
|
||||||
|
linesPerPage = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkStart := 0
|
||||||
|
for chunkStart < len(allLines) {
|
||||||
|
pb.ensureContentBelowHeader(pdf)
|
||||||
currentY := pdf.GetY()
|
currentY := pdf.GetY()
|
||||||
if currentY+totalHeight > pageHeight-bottomMargin {
|
// 本页剩余高度不足则换页再从页眉下开始
|
||||||
|
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()
|
pdf.AddPage()
|
||||||
currentY = ContentStartYBelowHeader
|
currentY = ContentStartYBelowHeader
|
||||||
pdf.SetY(currentY)
|
pdf.SetY(currentY)
|
||||||
}
|
}
|
||||||
|
|
||||||
startY := currentY
|
startY := currentY
|
||||||
// 绘制表格边框
|
|
||||||
pdf.SetDrawColor(180, 180, 180)
|
pdf.SetDrawColor(180, 180, 180)
|
||||||
pdf.Rect(startX, startY, tableWidth, totalHeight, "D")
|
pdf.Rect(startX, startY, tableWidth, chunkHeight, "D")
|
||||||
pdf.SetDrawColor(0, 0, 0)
|
pdf.SetDrawColor(0, 0, 0)
|
||||||
// 表格内内容左对齐
|
|
||||||
pdf.SetY(startY + padding)
|
pdf.SetY(startY + padding)
|
||||||
for _, line := range lines {
|
for _, line := range chunk {
|
||||||
pdf.SetX(startX + padding)
|
pdf.SetX(startX + padding)
|
||||||
pdf.CellFormat(innerWidth, lineHeight, line, "", 1, "L", false, 0, "")
|
pdf.CellFormat(innerWidth, lineHeight, line, "", 1, "L", false, 0, "")
|
||||||
}
|
}
|
||||||
pdf.SetY(startY + totalHeight)
|
pdf.SetY(startY + chunkHeight)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// safeSplitText 安全地分割文本,避免在没有中文字体时调用SplitText导致panic
|
// safeSplitText 安全地分割文本,避免在没有中文字体时调用SplitText导致panic
|
||||||
@@ -1295,7 +1424,7 @@ func (pb *PageBuilder) addAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.Product
|
|||||||
)
|
)
|
||||||
|
|
||||||
pdf.Ln(5)
|
pdf.Ln(5)
|
||||||
pb.fontManager.SetFont(pdf, "", 11)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
_, lineHt = pdf.GetFontSize()
|
_, lineHt = pdf.GetFontSize()
|
||||||
|
|
||||||
// 处理说明文本,按行分割并显示
|
// 处理说明文本,按行分割并显示
|
||||||
@@ -1439,7 +1568,7 @@ func (pb *PageBuilder) addQRCodeSection(pdf *gofpdf.Fpdf, doc *entities.ProductD
|
|||||||
|
|
||||||
// 二维码说明文字(简化版,放在二维码之后)
|
// 二维码说明文字(简化版,放在二维码之后)
|
||||||
pdf.Ln(10)
|
pdf.Ln(10)
|
||||||
pb.fontManager.SetFont(pdf, "", 11)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
_, lineHt = pdf.GetFontSize()
|
_, lineHt = pdf.GetFontSize()
|
||||||
|
|
||||||
qrCodeExplanation := "使用手机扫描上方二维码可直接跳转到天远API官网(https://tianyuanapi.com/),获取更多接口文档和资源。\n\n" +
|
qrCodeExplanation := "使用手机扫描上方二维码可直接跳转到天远API官网(https://tianyuanapi.com/),获取更多接口文档和资源。\n\n" +
|
||||||
@@ -1511,7 +1640,7 @@ func (pb *PageBuilder) addQRCodeImage(pdf *gofpdf.Fpdf, content string, chineseF
|
|||||||
|
|
||||||
// 添加二维码说明
|
// 添加二维码说明
|
||||||
pdf.Ln(10)
|
pdf.Ln(10)
|
||||||
pb.fontManager.SetFont(pdf, "", 10)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
_, lineHt := pdf.GetFontSize()
|
_, lineHt := pdf.GetFontSize()
|
||||||
pdf.CellFormat(0, lineHt, "官网二维码:", "", 1, "L", false, 0, "")
|
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)
|
pdf.SetY(pdf.GetY() + qrSize + 5)
|
||||||
pb.fontManager.SetFont(pdf, "", 9)
|
pb.fontManager.SetBodyFont(pdf, "", BodyFontSizeXiaosi)
|
||||||
_, lineHt = pdf.GetFontSize()
|
_, lineHt = pdf.GetFontSize()
|
||||||
qrNote := "使用手机扫描上方二维码可访问官网获取更多详情"
|
qrNote := "使用手机扫描上方二维码可访问官网获取更多详情"
|
||||||
noteWidth := pdf.GetStringWidth(qrNote)
|
noteWidth := pdf.GetStringWidth(qrNote)
|
||||||
|
|||||||
@@ -153,18 +153,23 @@ func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *ent
|
|||||||
pdf := gofpdf.New("P", "mm", "A4", "")
|
pdf := gofpdf.New("P", "mm", "A4", "")
|
||||||
// 上边距与 ContentStartYBelowHeader 一致,这样自动分页后新页内容从 logo 下方开始,不被遮挡
|
// 上边距与 ContentStartYBelowHeader 一致,这样自动分页后新页内容从 logo 下方开始,不被遮挡
|
||||||
pdf.SetMargins(15, ContentStartYBelowHeader, 15)
|
pdf.SetMargins(15, ContentStartYBelowHeader, 15)
|
||||||
|
// 开启自动分页并预留底边距,避免内容贴底;分页后由 SetHeaderFunc 绘制页眉,正文从 ContentStartYBelowHeader 起排
|
||||||
|
pdf.SetAutoPageBreak(true, 18)
|
||||||
|
|
||||||
// 加载黑体字体(用于所有内容,除了水印)
|
// 加载黑体字体(用于所有内容,除了水印)
|
||||||
// 注意:此时工作目录应该是根目录(/),这样gofpdf处理路径时就能正确解析
|
// 注意:此时工作目录应该是根目录(/),这样gofpdf处理路径时就能正确解析
|
||||||
chineseFontAvailable := g.fontManager.LoadChineseFont(pdf)
|
chineseFontAvailable := g.fontManager.LoadChineseFont(pdf)
|
||||||
|
|
||||||
// 加载水印字体(使用宋体或其他非黑体字体)
|
// 加载水印字体
|
||||||
watermarkFontAvailable := g.fontManager.LoadWatermarkFont(pdf)
|
watermarkFontAvailable := g.fontManager.LoadWatermarkFont(pdf)
|
||||||
|
// 加载正文宋体(描述、详情、说明、表格文字等使用小四 12pt)
|
||||||
|
bodyFontAvailable := g.fontManager.LoadBodyFont(pdf)
|
||||||
|
|
||||||
// 记录字体加载状态,便于诊断问题
|
// 记录字体加载状态,便于诊断问题
|
||||||
g.logger.Info("PDF字体加载状态",
|
g.logger.Info("PDF字体加载状态",
|
||||||
zap.Bool("chinese_font_loaded", chineseFontAvailable),
|
zap.Bool("chinese_font_loaded", chineseFontAvailable),
|
||||||
zap.Bool("watermark_font_loaded", watermarkFontAvailable),
|
zap.Bool("watermark_font_loaded", watermarkFontAvailable),
|
||||||
|
zap.Bool("body_font_loaded", bodyFontAvailable),
|
||||||
zap.String("watermark_text", g.watermarkText),
|
zap.String("watermark_text", g.watermarkText),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -176,9 +181,11 @@ func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *ent
|
|||||||
// 创建页面构建器
|
// 创建页面构建器
|
||||||
pageBuilder := NewPageBuilder(g.logger, g.fontManager, g.textProcessor, g.markdownProc, g.tableParser, g.tableRenderer, g.jsonProcessor, g.logoPath, g.watermarkText)
|
pageBuilder := NewPageBuilder(g.logger, g.fontManager, g.textProcessor, g.markdownProc, g.tableParser, g.tableRenderer, g.jsonProcessor, g.logoPath, g.watermarkText)
|
||||||
|
|
||||||
// 使用 SetHeaderFunc 确保每页(包括表格等内部调用 AddPage 的页)都会先绘制页眉+水印
|
// 页眉只绘制 logo 和横线;水印改到页脚绘制,确保水印在最上层不被表格等内容遮挡
|
||||||
pdf.SetHeaderFunc(func() {
|
pdf.SetHeaderFunc(func() {
|
||||||
pageBuilder.addHeader(pdf, chineseFontAvailable)
|
pageBuilder.addHeader(pdf, chineseFontAvailable)
|
||||||
|
})
|
||||||
|
pdf.SetFooterFunc(func() {
|
||||||
pageBuilder.addWatermark(pdf, chineseFontAvailable)
|
pageBuilder.addWatermark(pdf, chineseFontAvailable)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,130 @@ func (tp *TextProcessor) HTMLToPlainWithBreaks(text string) string {
|
|||||||
return strings.TrimSpace(text)
|
return strings.TrimSpace(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTMLSegment 用于 PDF 绘制的 HTML 片段:支持段落、换行、加粗、标题
|
||||||
|
type HTMLSegment struct {
|
||||||
|
Text string // 纯文本(已去标签、已解码实体)
|
||||||
|
Bold bool // 是否加粗
|
||||||
|
NewLine bool // 是否换行(如 <br>)
|
||||||
|
NewParagraph bool // 是否新段落(如 </p>、</div>)
|
||||||
|
HeadingLevel int // 1-3 表示 h1-h3,0 表示正文
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseHTMLToSegments 将 HTML 解析为用于 PDF 绘制的片段序列,保留段落、换行、加粗与标题
|
||||||
|
func (tp *TextProcessor) ParseHTMLToSegments(htmlStr string) []HTMLSegment {
|
||||||
|
htmlStr = html.UnescapeString(htmlStr)
|
||||||
|
var out []HTMLSegment
|
||||||
|
blockSplit := regexp.MustCompile(`(?i)(</p>|</div>|</h[1-6]>|<br\s*/?>)\s*`)
|
||||||
|
parts := blockSplit.Split(htmlStr, -1)
|
||||||
|
tags := blockSplit.FindAllString(htmlStr, -1)
|
||||||
|
for i, block := range parts {
|
||||||
|
block = strings.TrimSpace(block)
|
||||||
|
var prevTag string
|
||||||
|
if i > 0 && i-1 < len(tags) {
|
||||||
|
prevTag = strings.ToLower(strings.TrimSpace(tags[i-1]))
|
||||||
|
}
|
||||||
|
isNewParagraph := strings.Contains(prevTag, "</p>") || strings.Contains(prevTag, "</div>") ||
|
||||||
|
strings.HasPrefix(prevTag, "</h")
|
||||||
|
isNewLine := strings.Contains(prevTag, "<br")
|
||||||
|
headingLevel := 0
|
||||||
|
if strings.HasPrefix(prevTag, "</h1") {
|
||||||
|
headingLevel = 1
|
||||||
|
} else if strings.HasPrefix(prevTag, "</h2") {
|
||||||
|
headingLevel = 2
|
||||||
|
} else if strings.HasPrefix(prevTag, "</h3") {
|
||||||
|
headingLevel = 3
|
||||||
|
}
|
||||||
|
segments := tp.parseInlineSegments(block)
|
||||||
|
// 块前先输出段落/换行/标题标记(仅在第一段文本前输出一次)
|
||||||
|
if i > 0 {
|
||||||
|
if isNewParagraph || headingLevel > 0 {
|
||||||
|
out = append(out, HTMLSegment{NewParagraph: true, HeadingLevel: headingLevel})
|
||||||
|
} else if isNewLine {
|
||||||
|
out = append(out, HTMLSegment{NewLine: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, seg := range segments {
|
||||||
|
if seg.Text != "" {
|
||||||
|
out = append(out, HTMLSegment{Text: seg.Text, Bold: seg.Bold, HeadingLevel: headingLevel})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// inlineSeg 内联片段(文本 + 是否加粗)
|
||||||
|
type inlineSeg struct {
|
||||||
|
Text string
|
||||||
|
Bold bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInlineSegments 解析块内文本,按 <strong>/<b> 拆成片段
|
||||||
|
func (tp *TextProcessor) parseInlineSegments(block string) []inlineSeg {
|
||||||
|
var segs []inlineSeg
|
||||||
|
// 移除所有标签并收集加粗区间(按字符偏移)
|
||||||
|
reBoldOpen := regexp.MustCompile(`(?i)<(strong|b)>`)
|
||||||
|
reBoldClose := regexp.MustCompile(`(?i)</(strong|b)>`)
|
||||||
|
plain := regexp.MustCompile(`<[^>]+>`).ReplaceAllString(block, "")
|
||||||
|
plain = regexp.MustCompile(`[ \t]+`).ReplaceAllString(plain, " ")
|
||||||
|
plain = strings.TrimSpace(plain)
|
||||||
|
if plain == "" {
|
||||||
|
return segs
|
||||||
|
}
|
||||||
|
// 在原始 block 上找加粗区间,再映射到 plain(去掉标签后的位置)
|
||||||
|
work := block
|
||||||
|
var boldRanges [][2]int
|
||||||
|
offset := 0
|
||||||
|
for {
|
||||||
|
idxOpen := reBoldOpen.FindStringIndex(work)
|
||||||
|
if idxOpen == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
afterOpen := work[idxOpen[1]:]
|
||||||
|
idxClose := reBoldClose.FindStringIndex(afterOpen)
|
||||||
|
if idxClose == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
startInWork := offset + idxOpen[1]
|
||||||
|
endInWork := offset + idxOpen[1] + idxClose[0]
|
||||||
|
// 将 work 坐标映射到 plain:需要数 plain 中对应字符
|
||||||
|
workBefore := work[:startInWork]
|
||||||
|
plainBefore := regexp.MustCompile(`<[^>]+>`).ReplaceAllString(workBefore, "")
|
||||||
|
plainBefore = regexp.MustCompile(`[ \t]+`).ReplaceAllString(plainBefore, " ")
|
||||||
|
startPlain := len([]rune(plainBefore))
|
||||||
|
workUntil := work[:endInWork]
|
||||||
|
plainUntil := regexp.MustCompile(`<[^>]+>`).ReplaceAllString(workUntil, "")
|
||||||
|
plainUntil = regexp.MustCompile(`[ \t]+`).ReplaceAllString(plainUntil, " ")
|
||||||
|
endPlain := len([]rune(plainUntil))
|
||||||
|
boldRanges = append(boldRanges, [2]int{startPlain, endPlain})
|
||||||
|
work = work[endInWork+len(reBoldClose.FindString(afterOpen)):]
|
||||||
|
offset = endInWork + len(reBoldClose.FindString(afterOpen))
|
||||||
|
}
|
||||||
|
// 按 boldRanges 切分 plain
|
||||||
|
runes := []rune(plain)
|
||||||
|
inBold := false
|
||||||
|
var start int
|
||||||
|
for i := 0; i <= len(runes); i++ {
|
||||||
|
nowBold := false
|
||||||
|
for _, r := range boldRanges {
|
||||||
|
if i >= r[0] && i < r[1] {
|
||||||
|
nowBold = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nowBold != inBold || i == len(runes) {
|
||||||
|
if i > start {
|
||||||
|
segs = append(segs, inlineSeg{Text: string(runes[start:i]), Bold: inBold})
|
||||||
|
}
|
||||||
|
start = i
|
||||||
|
inBold = nowBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(segs) == 0 && plain != "" {
|
||||||
|
segs = append(segs, inlineSeg{Text: plain, Bold: false})
|
||||||
|
}
|
||||||
|
return segs
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveMarkdownSyntax 移除markdown语法,保留纯文本
|
// RemoveMarkdownSyntax 移除markdown语法,保留纯文本
|
||||||
func (tp *TextProcessor) RemoveMarkdownSyntax(text string) string {
|
func (tp *TextProcessor) RemoveMarkdownSyntax(text string) string {
|
||||||
// 移除粗体标记 **text** 或 __text__
|
// 移除粗体标记 **text** 或 __text__
|
||||||
|
|||||||
Reference in New Issue
Block a user