2025-12-03 12:03:42 +08:00
|
|
|
|
package pdf
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"math"
|
2025-12-03 18:02:49 +08:00
|
|
|
|
"regexp"
|
2025-12-03 12:03:42 +08:00
|
|
|
|
"strings"
|
2025-12-03 18:02:49 +08:00
|
|
|
|
"unicode"
|
2025-12-03 12:03:42 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/jung-kurt/gofpdf/v2"
|
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// DatabaseTableRenderer 数据库表格渲染器
|
|
|
|
|
|
type DatabaseTableRenderer struct {
|
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
|
fontManager *FontManager
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewDatabaseTableRenderer 创建数据库表格渲染器
|
|
|
|
|
|
func NewDatabaseTableRenderer(logger *zap.Logger, fontManager *FontManager) *DatabaseTableRenderer {
|
|
|
|
|
|
return &DatabaseTableRenderer{
|
|
|
|
|
|
logger: logger,
|
|
|
|
|
|
fontManager: fontManager,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RenderTable 渲染表格到PDF
|
|
|
|
|
|
func (r *DatabaseTableRenderer) RenderTable(pdf *gofpdf.Fpdf, tableData *TableData) error {
|
|
|
|
|
|
if tableData == nil || len(tableData.Headers) == 0 {
|
|
|
|
|
|
r.logger.Warn("表格数据为空,跳过渲染")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
r.logger.Info("开始渲染表格",
|
|
|
|
|
|
zap.Int("header_count", len(tableData.Headers)),
|
|
|
|
|
|
zap.Int("row_count", len(tableData.Rows)),
|
|
|
|
|
|
zap.Strings("headers", tableData.Headers))
|
|
|
|
|
|
|
|
|
|
|
|
// 检查表头是否有有效内容
|
|
|
|
|
|
hasValidHeader := false
|
|
|
|
|
|
for _, header := range tableData.Headers {
|
|
|
|
|
|
if strings.TrimSpace(header) != "" {
|
|
|
|
|
|
hasValidHeader = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !hasValidHeader {
|
|
|
|
|
|
r.logger.Warn("表头内容为空,跳过渲染")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有有效的数据行
|
|
|
|
|
|
hasValidRows := false
|
|
|
|
|
|
for _, row := range tableData.Rows {
|
|
|
|
|
|
for _, cell := range row {
|
|
|
|
|
|
if strings.TrimSpace(cell) != "" {
|
|
|
|
|
|
hasValidRows = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if hasValidRows {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
r.logger.Debug("表格验证通过",
|
|
|
|
|
|
zap.Bool("has_valid_header", hasValidHeader),
|
|
|
|
|
|
zap.Bool("has_valid_rows", hasValidRows),
|
|
|
|
|
|
zap.Int("row_count", len(tableData.Rows)))
|
|
|
|
|
|
|
|
|
|
|
|
// 即使没有数据行,也渲染表头(单行表格)
|
|
|
|
|
|
// 但如果没有表头也没有数据,则不渲染
|
|
|
|
|
|
|
|
|
|
|
|
// 设置字体
|
|
|
|
|
|
r.fontManager.SetFont(pdf, "", 9)
|
|
|
|
|
|
_, lineHt := pdf.GetFontSize()
|
|
|
|
|
|
|
|
|
|
|
|
// 计算页面可用宽度
|
|
|
|
|
|
pageWidth, _ := pdf.GetPageSize()
|
|
|
|
|
|
availableWidth := pageWidth - 30 // 减去左右边距(15mm * 2)
|
|
|
|
|
|
|
|
|
|
|
|
// 计算每列宽度
|
|
|
|
|
|
colWidths := r.calculateColumnWidths(pdf, tableData, availableWidth)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否需要分页(在绘制表头前)
|
|
|
|
|
|
_, pageHeight := pdf.GetPageSize()
|
|
|
|
|
|
_, _, _, bottomMargin := pdf.GetMargins()
|
|
|
|
|
|
currentY := pdf.GetY()
|
|
|
|
|
|
estimatedHeaderHeight := lineHt * 2.5 // 估算表头高度
|
|
|
|
|
|
|
|
|
|
|
|
if currentY+estimatedHeaderHeight > pageHeight-bottomMargin {
|
|
|
|
|
|
r.logger.Debug("表头前需要分页", zap.Float64("current_y", currentY))
|
|
|
|
|
|
pdf.AddPage()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制表头
|
|
|
|
|
|
headerStartY := pdf.GetY()
|
|
|
|
|
|
headerHeight := r.renderHeader(pdf, tableData.Headers, colWidths, headerStartY)
|
|
|
|
|
|
|
|
|
|
|
|
// 移动到表头下方
|
|
|
|
|
|
pdf.SetXY(15.0, headerStartY+headerHeight)
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制数据行(如果有数据行)
|
|
|
|
|
|
if len(tableData.Rows) > 0 {
|
|
|
|
|
|
r.logger.Debug("开始渲染数据行", zap.Int("row_count", len(tableData.Rows)))
|
|
|
|
|
|
r.renderRows(pdf, tableData.Rows, colWidths, lineHt, tableData.Headers, colWidths)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
r.logger.Debug("没有数据行,只渲染表头")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
r.logger.Info("表格渲染完成",
|
|
|
|
|
|
zap.Int("header_count", len(tableData.Headers)),
|
|
|
|
|
|
zap.Int("row_count", len(tableData.Rows)),
|
|
|
|
|
|
zap.Float64("current_y", pdf.GetY()))
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// calculateColumnWidths 计算每列的宽度
|
|
|
|
|
|
// 策略:先确保每列最短内容能完整显示(不换行),然后根据内容长度分配剩余空间
|
|
|
|
|
|
func (r *DatabaseTableRenderer) calculateColumnWidths(pdf *gofpdf.Fpdf, tableData *TableData, availableWidth float64) []float64 {
|
|
|
|
|
|
numCols := len(tableData.Headers)
|
|
|
|
|
|
r.fontManager.SetFont(pdf, "", 9)
|
|
|
|
|
|
|
|
|
|
|
|
// 第一步:找到每列中最短的内容(包括表头),计算其完整显示所需的最小宽度
|
|
|
|
|
|
colMinWidths := make([]float64, numCols)
|
|
|
|
|
|
colMaxWidths := make([]float64, numCols)
|
|
|
|
|
|
colContentLengths := make([]float64, numCols) // 用于记录内容总长度,用于分配剩余空间
|
|
|
|
|
|
|
|
|
|
|
|
for i := 0; i < numCols; i++ {
|
|
|
|
|
|
minWidth := math.MaxFloat64
|
|
|
|
|
|
maxWidth := 0.0
|
|
|
|
|
|
totalLength := 0.0
|
|
|
|
|
|
count := 0
|
|
|
|
|
|
|
|
|
|
|
|
// 检查表头
|
|
|
|
|
|
if i < len(tableData.Headers) {
|
|
|
|
|
|
header := tableData.Headers[i]
|
|
|
|
|
|
textWidth := r.getTextWidth(pdf, header)
|
|
|
|
|
|
cellWidth := textWidth + 8 // 加上内边距
|
|
|
|
|
|
if cellWidth < minWidth {
|
|
|
|
|
|
minWidth = cellWidth
|
|
|
|
|
|
}
|
|
|
|
|
|
if cellWidth > maxWidth {
|
|
|
|
|
|
maxWidth = cellWidth
|
|
|
|
|
|
}
|
|
|
|
|
|
totalLength += textWidth
|
|
|
|
|
|
count++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查所有数据行
|
|
|
|
|
|
for _, row := range tableData.Rows {
|
|
|
|
|
|
if i < len(row) {
|
|
|
|
|
|
cell := row[i]
|
|
|
|
|
|
textWidth := r.getTextWidth(pdf, cell)
|
|
|
|
|
|
cellWidth := textWidth + 8 // 加上内边距
|
|
|
|
|
|
if cellWidth < minWidth {
|
|
|
|
|
|
minWidth = cellWidth
|
|
|
|
|
|
}
|
|
|
|
|
|
if cellWidth > maxWidth {
|
|
|
|
|
|
maxWidth = cellWidth
|
|
|
|
|
|
}
|
|
|
|
|
|
totalLength += textWidth
|
|
|
|
|
|
count++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置最小宽度(确保最短内容能完整显示)
|
|
|
|
|
|
if minWidth == math.MaxFloat64 {
|
|
|
|
|
|
colMinWidths[i] = 30.0 // 默认最小宽度
|
|
|
|
|
|
} else {
|
|
|
|
|
|
colMinWidths[i] = math.Max(minWidth, 25.0) // 至少25mm
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
colMaxWidths[i] = maxWidth
|
|
|
|
|
|
if count > 0 {
|
|
|
|
|
|
colContentLengths[i] = totalLength / float64(count) // 平均内容长度
|
|
|
|
|
|
} else {
|
|
|
|
|
|
colContentLengths[i] = colMinWidths[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第二步:计算总的最小宽度(确保所有最短内容都能显示)
|
|
|
|
|
|
totalMinWidth := 0.0
|
|
|
|
|
|
for _, w := range colMinWidths {
|
|
|
|
|
|
totalMinWidth += w
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 第三步:分配宽度
|
|
|
|
|
|
colWidths := make([]float64, numCols)
|
|
|
|
|
|
if totalMinWidth >= availableWidth {
|
|
|
|
|
|
// 如果最小宽度已经超过可用宽度,按比例缩放,但确保每列至少能显示最短内容
|
|
|
|
|
|
scale := availableWidth / totalMinWidth
|
|
|
|
|
|
for i := range colWidths {
|
|
|
|
|
|
colWidths[i] = colMinWidths[i] * scale
|
|
|
|
|
|
// 确保最小宽度,但允许稍微压缩
|
|
|
|
|
|
if colWidths[i] < colMinWidths[i]*0.8 {
|
|
|
|
|
|
colWidths[i] = colMinWidths[i] * 0.8
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果最小宽度小于可用宽度,先分配最小宽度,然后根据内容长度分配剩余空间
|
|
|
|
|
|
extraWidth := availableWidth - totalMinWidth
|
|
|
|
|
|
|
|
|
|
|
|
// 计算总的内容长度(用于按比例分配)
|
|
|
|
|
|
totalContentLength := 0.0
|
|
|
|
|
|
for _, length := range colContentLengths {
|
|
|
|
|
|
totalContentLength += length
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果总内容长度为0,平均分配
|
|
|
|
|
|
if totalContentLength < 0.1 {
|
|
|
|
|
|
extraPerCol := extraWidth / float64(numCols)
|
|
|
|
|
|
for i := range colWidths {
|
|
|
|
|
|
colWidths[i] = colMinWidths[i] + extraPerCol
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 根据内容长度按比例分配剩余空间
|
|
|
|
|
|
for i := range colWidths {
|
|
|
|
|
|
// 计算这一列应该分配多少额外空间(基于内容长度)
|
|
|
|
|
|
ratio := colContentLengths[i] / totalContentLength
|
|
|
|
|
|
extraForCol := extraWidth * ratio
|
|
|
|
|
|
colWidths[i] = colMinWidths[i] + extraForCol
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return colWidths
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 18:02:49 +08:00
|
|
|
|
// cleanTextForPDF 清理文本,移除可能导致PDF生成问题的字符
|
|
|
|
|
|
func (r *DatabaseTableRenderer) cleanTextForPDF(text string) string {
|
|
|
|
|
|
// 先移除HTML标签
|
|
|
|
|
|
text = strings.TrimSpace(text)
|
|
|
|
|
|
text = strings.ReplaceAll(text, "<br>", " ")
|
|
|
|
|
|
text = strings.ReplaceAll(text, "<br/>", " ")
|
|
|
|
|
|
text = strings.ReplaceAll(text, "<br />", " ")
|
|
|
|
|
|
|
|
|
|
|
|
// 移除控制字符(除了换行符和制表符)
|
|
|
|
|
|
var cleaned strings.Builder
|
|
|
|
|
|
for _, r := range text {
|
|
|
|
|
|
// 保留可打印字符、空格、换行符、制表符
|
|
|
|
|
|
if unicode.IsPrint(r) || r == '\n' || r == '\t' || r == '\r' {
|
|
|
|
|
|
cleaned.WriteRune(r)
|
|
|
|
|
|
} else if unicode.IsSpace(r) {
|
|
|
|
|
|
// 将其他空白字符转换为空格
|
|
|
|
|
|
cleaned.WriteRune(' ')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
text = cleaned.String()
|
|
|
|
|
|
|
|
|
|
|
|
// 移除多余的空白字符
|
|
|
|
|
|
text = regexp.MustCompile(`\s+`).ReplaceAllString(text, " ")
|
|
|
|
|
|
|
|
|
|
|
|
// 确保文本不为空且长度合理
|
|
|
|
|
|
if len([]rune(text)) > 10000 {
|
|
|
|
|
|
// 如果文本过长,截断
|
|
|
|
|
|
runes := []rune(text)
|
|
|
|
|
|
text = string(runes[:10000]) + "..."
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return strings.TrimSpace(text)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// safeSplitText 安全地调用SplitText,带错误恢复
|
|
|
|
|
|
func (r *DatabaseTableRenderer) safeSplitText(pdf *gofpdf.Fpdf, text string, width float64) []string {
|
|
|
|
|
|
// 先清理文本
|
|
|
|
|
|
text = r.cleanTextForPDF(text)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果文本为空或宽度无效,返回单行
|
|
|
|
|
|
if text == "" || width <= 0 {
|
2025-12-04 10:35:11 +08:00
|
|
|
|
if text == "" {
|
|
|
|
|
|
return []string{""}
|
|
|
|
|
|
}
|
|
|
|
|
|
return []string{text}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查文本长度,如果太短可能不需要分割
|
|
|
|
|
|
if len([]rune(text)) <= 1 {
|
2025-12-03 18:02:49 +08:00
|
|
|
|
return []string{text}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用匿名函数和recover保护SplitText调用
|
|
|
|
|
|
var lines []string
|
2025-12-04 10:35:11 +08:00
|
|
|
|
var panicOccurred bool
|
2025-12-03 18:02:49 +08:00
|
|
|
|
func() {
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if rec := recover(); rec != nil {
|
2025-12-04 10:35:11 +08:00
|
|
|
|
panicOccurred = true
|
|
|
|
|
|
// 静默处理,不记录日志
|
2025-12-03 18:02:49 +08:00
|
|
|
|
// 如果panic发生,使用估算值
|
|
|
|
|
|
charCount := len([]rune(text))
|
2025-12-04 10:35:11 +08:00
|
|
|
|
if charCount == 0 {
|
|
|
|
|
|
lines = []string{""}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-03 18:02:49 +08:00
|
|
|
|
estimatedLines := math.Max(1, math.Ceil(float64(charCount)/20))
|
|
|
|
|
|
lines = make([]string, int(estimatedLines))
|
|
|
|
|
|
if estimatedLines == 1 {
|
|
|
|
|
|
lines[0] = text
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 简单分割文本
|
|
|
|
|
|
runes := []rune(text)
|
|
|
|
|
|
charsPerLine := int(math.Ceil(float64(len(runes)) / estimatedLines))
|
|
|
|
|
|
for i := 0; i < int(estimatedLines); i++ {
|
|
|
|
|
|
start := i * charsPerLine
|
|
|
|
|
|
end := start + charsPerLine
|
|
|
|
|
|
if end > len(runes) {
|
|
|
|
|
|
end = len(runes)
|
|
|
|
|
|
}
|
|
|
|
|
|
if start < len(runes) {
|
|
|
|
|
|
lines[i] = string(runes[start:end])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lines[i] = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
// 尝试调用SplitText
|
|
|
|
|
|
lines = pdf.SplitText(text, width)
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
2025-12-04 10:35:11 +08:00
|
|
|
|
// 如果panic发生或lines为nil或空,使用后备方案
|
|
|
|
|
|
if panicOccurred || lines == nil || len(lines) == 0 {
|
|
|
|
|
|
// 如果文本不为空,至少返回一行
|
|
|
|
|
|
if text != "" {
|
|
|
|
|
|
return []string{text}
|
|
|
|
|
|
}
|
|
|
|
|
|
return []string{""}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤掉空行(但保留至少一行)
|
|
|
|
|
|
nonEmptyLines := make([]string, 0, len(lines))
|
|
|
|
|
|
for _, line := range lines {
|
|
|
|
|
|
if strings.TrimSpace(line) != "" {
|
|
|
|
|
|
nonEmptyLines = append(nonEmptyLines, line)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(nonEmptyLines) == 0 {
|
2025-12-03 18:02:49 +08:00
|
|
|
|
return []string{text}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 10:35:11 +08:00
|
|
|
|
return nonEmptyLines
|
2025-12-03 18:02:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
// renderHeader 渲染表头
|
|
|
|
|
|
func (r *DatabaseTableRenderer) renderHeader(pdf *gofpdf.Fpdf, headers []string, colWidths []float64, startY float64) float64 {
|
|
|
|
|
|
_, lineHt := pdf.GetFontSize()
|
|
|
|
|
|
|
|
|
|
|
|
// 计算表头的最大高度
|
|
|
|
|
|
maxHeaderHeight := lineHt * 2.0 // 使用合理的表头高度
|
|
|
|
|
|
for i, header := range headers {
|
|
|
|
|
|
if i >= len(colWidths) {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
colW := colWidths[i]
|
2025-12-03 18:02:49 +08:00
|
|
|
|
// 使用安全的SplitText方法
|
|
|
|
|
|
headerLines := r.safeSplitText(pdf, header, colW-6)
|
2025-12-03 12:03:42 +08:00
|
|
|
|
headerHeight := float64(len(headerLines)) * lineHt
|
|
|
|
|
|
// 添加上下内边距
|
|
|
|
|
|
headerHeight += lineHt * 0.8 // 上下各0.4倍行高的内边距
|
|
|
|
|
|
if headerHeight < lineHt*2.0 {
|
|
|
|
|
|
headerHeight = lineHt * 2.0
|
|
|
|
|
|
}
|
|
|
|
|
|
if headerHeight > maxHeaderHeight {
|
|
|
|
|
|
maxHeaderHeight = headerHeight
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制表头背景和文本
|
|
|
|
|
|
pdf.SetFillColor(74, 144, 226) // 蓝色背景
|
|
|
|
|
|
pdf.SetTextColor(0, 0, 0) // 黑色文字
|
|
|
|
|
|
r.fontManager.SetFont(pdf, "B", 9)
|
|
|
|
|
|
|
|
|
|
|
|
currentX := 15.0
|
|
|
|
|
|
for i, header := range headers {
|
|
|
|
|
|
if i >= len(colWidths) {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
colW := colWidths[i]
|
|
|
|
|
|
|
|
|
|
|
|
// 清理表头数据,移除任何残留的HTML标签和样式
|
2025-12-03 18:02:49 +08:00
|
|
|
|
header = r.cleanTextForPDF(header)
|
2025-12-03 12:03:42 +08:00
|
|
|
|
|
|
|
|
|
|
// 绘制表头背景
|
|
|
|
|
|
pdf.Rect(currentX, startY, colW, maxHeaderHeight, "FD")
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制表头文本
|
|
|
|
|
|
if header != "" {
|
|
|
|
|
|
// 计算文本的实际高度(减少内边距,给文本更多空间)
|
2025-12-03 18:02:49 +08:00
|
|
|
|
// 使用安全的SplitText方法
|
|
|
|
|
|
headerLines := r.safeSplitText(pdf, header, colW-4)
|
2025-12-03 12:03:42 +08:00
|
|
|
|
textHeight := float64(len(headerLines)) * lineHt
|
|
|
|
|
|
if textHeight < lineHt {
|
|
|
|
|
|
textHeight = lineHt
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算垂直居中的Y位置
|
|
|
|
|
|
cellCenterY := startY + maxHeaderHeight/2
|
|
|
|
|
|
textStartY := cellCenterY - textHeight/2
|
|
|
|
|
|
|
|
|
|
|
|
// 设置文本位置(水平居中,垂直居中,减少左边距)
|
|
|
|
|
|
pdf.SetXY(currentX+2, textStartY)
|
|
|
|
|
|
// 确保颜色为深黑色(在渲染前再次设置,防止被覆盖)
|
|
|
|
|
|
pdf.SetTextColor(0, 0, 0) // 表头是黑色文字
|
|
|
|
|
|
// 设置字体,确保颜色不会变淡
|
|
|
|
|
|
r.fontManager.SetFont(pdf, "B", 9)
|
|
|
|
|
|
// 再次确保颜色为深黑色(在渲染前最后一次设置)
|
|
|
|
|
|
pdf.SetTextColor(0, 0, 0)
|
|
|
|
|
|
// 使用正常的行高,文本已经垂直居中(减少内边距,给文本更多空间)
|
|
|
|
|
|
pdf.MultiCell(colW-4, lineHt, header, "", "C", false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置Y坐标
|
|
|
|
|
|
pdf.SetXY(currentX+colW, startY)
|
|
|
|
|
|
currentX += colW
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return maxHeaderHeight
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// renderRows 渲染数据行(支持自动分页)
|
|
|
|
|
|
func (r *DatabaseTableRenderer) renderRows(pdf *gofpdf.Fpdf, rows [][]string, colWidths []float64, lineHt float64, headers []string, headerColWidths []float64) {
|
|
|
|
|
|
numCols := len(colWidths)
|
|
|
|
|
|
pdf.SetFillColor(245, 245, 220) // 米色背景
|
|
|
|
|
|
pdf.SetTextColor(0, 0, 0) // 深黑色文字,确保清晰
|
|
|
|
|
|
r.fontManager.SetFont(pdf, "", 9)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取页面尺寸和边距
|
|
|
|
|
|
_, pageHeight := pdf.GetPageSize()
|
|
|
|
|
|
_, _, _, bottomMargin := pdf.GetMargins()
|
|
|
|
|
|
minSpaceForRow := lineHt * 3 // 至少需要3倍行高的空间
|
|
|
|
|
|
|
|
|
|
|
|
validRowIndex := 0 // 用于交替填充的有效行索引
|
|
|
|
|
|
for rowIndex, row := range rows {
|
|
|
|
|
|
// 检查这一行是否有有效内容
|
|
|
|
|
|
hasContent := false
|
|
|
|
|
|
for _, cell := range row {
|
|
|
|
|
|
if strings.TrimSpace(cell) != "" {
|
|
|
|
|
|
hasContent = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 跳过完全为空的行
|
|
|
|
|
|
if !hasContent {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否需要分页
|
|
|
|
|
|
currentY := pdf.GetY()
|
|
|
|
|
|
if currentY+minSpaceForRow > pageHeight-bottomMargin {
|
|
|
|
|
|
// 需要分页
|
|
|
|
|
|
r.logger.Debug("表格需要分页",
|
|
|
|
|
|
zap.Int("row_index", rowIndex),
|
|
|
|
|
|
zap.Float64("current_y", currentY),
|
|
|
|
|
|
zap.Float64("page_height", pageHeight))
|
|
|
|
|
|
pdf.AddPage()
|
|
|
|
|
|
// 在新页面上重新绘制表头
|
|
|
|
|
|
if len(headers) > 0 && len(headerColWidths) > 0 {
|
|
|
|
|
|
newHeaderStartY := pdf.GetY()
|
|
|
|
|
|
headerHeight := r.renderHeader(pdf, headers, headerColWidths, newHeaderStartY)
|
|
|
|
|
|
pdf.SetXY(15.0, newHeaderStartY+headerHeight)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
startY := pdf.GetY()
|
|
|
|
|
|
fill := (validRowIndex % 2) == 0 // 交替填充
|
|
|
|
|
|
validRowIndex++
|
|
|
|
|
|
|
|
|
|
|
|
// 计算这一行的最大高度
|
2025-12-04 10:35:11 +08:00
|
|
|
|
// 确保lineHt有效
|
|
|
|
|
|
if lineHt <= 0 {
|
|
|
|
|
|
lineHt = 5.0 // 默认行高
|
|
|
|
|
|
}
|
2025-12-03 12:03:42 +08:00
|
|
|
|
maxCellHeight := lineHt * 2.0 // 使用合理的最小高度
|
|
|
|
|
|
for j := 0; j < numCols && j < len(row); j++ {
|
|
|
|
|
|
cell := row[j]
|
|
|
|
|
|
cellWidth := colWidths[j] - 4 // 减少内边距到4mm,给文本更多空间
|
|
|
|
|
|
|
2025-12-03 18:02:49 +08:00
|
|
|
|
// 先清理单元格文本(在计算宽度之前)
|
|
|
|
|
|
cell = r.cleanTextForPDF(cell)
|
|
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
// 计算文本实际宽度,判断是否需要换行
|
|
|
|
|
|
textWidth := r.getTextWidth(pdf, cell)
|
|
|
|
|
|
var lines []string
|
|
|
|
|
|
|
|
|
|
|
|
// 只有当文本宽度超过单元格宽度时才换行
|
|
|
|
|
|
if textWidth > cellWidth {
|
2025-12-03 18:02:49 +08:00
|
|
|
|
// 文本需要换行,使用安全的SplitText方法
|
|
|
|
|
|
lines = r.safeSplitText(pdf, cell, cellWidth)
|
2025-12-03 12:03:42 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 文本不需要换行,单行显示
|
|
|
|
|
|
lines = []string{cell}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 10:35:11 +08:00
|
|
|
|
// 确保lines不为空且有效
|
|
|
|
|
|
if len(lines) == 0 || (len(lines) == 1 && lines[0] == "") {
|
|
|
|
|
|
lines = []string{cell}
|
|
|
|
|
|
if lines[0] == "" {
|
|
|
|
|
|
lines[0] = " " // 至少保留一个空格,避免高度为0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算单元格高度,确保不会出现Inf或NaN
|
|
|
|
|
|
lineCount := float64(len(lines))
|
|
|
|
|
|
if lineCount <= 0 {
|
|
|
|
|
|
lineCount = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if lineHt <= 0 {
|
|
|
|
|
|
lineHt = 5.0 // 默认行高
|
|
|
|
|
|
}
|
|
|
|
|
|
cellHeight := lineCount * lineHt
|
2025-12-03 12:03:42 +08:00
|
|
|
|
// 添加上下内边距
|
|
|
|
|
|
cellHeight += lineHt * 0.8 // 上下各0.4倍行高的内边距
|
|
|
|
|
|
if cellHeight < lineHt*2.0 {
|
|
|
|
|
|
cellHeight = lineHt * 2.0
|
|
|
|
|
|
}
|
2025-12-04 10:35:11 +08:00
|
|
|
|
// 检查是否为有效数值
|
|
|
|
|
|
if math.IsInf(cellHeight, 0) || math.IsNaN(cellHeight) {
|
|
|
|
|
|
cellHeight = lineHt * 2.0
|
|
|
|
|
|
}
|
2025-12-03 12:03:42 +08:00
|
|
|
|
if cellHeight > maxCellHeight {
|
|
|
|
|
|
maxCellHeight = cellHeight
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 再次检查分页(在计算完行高后)
|
|
|
|
|
|
if startY+maxCellHeight > pageHeight-bottomMargin {
|
|
|
|
|
|
r.logger.Debug("行高度超出页面,需要分页",
|
|
|
|
|
|
zap.Int("row_index", rowIndex),
|
|
|
|
|
|
zap.Float64("row_height", maxCellHeight))
|
|
|
|
|
|
pdf.AddPage()
|
|
|
|
|
|
startY = pdf.GetY()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制这一行的所有单元格
|
|
|
|
|
|
currentX := 15.0
|
|
|
|
|
|
for j := 0; j < numCols && j < len(row); j++ {
|
|
|
|
|
|
colW := colWidths[j]
|
|
|
|
|
|
cell := row[j]
|
|
|
|
|
|
|
|
|
|
|
|
// 清理单元格数据,移除任何残留的HTML标签和样式
|
2025-12-03 18:02:49 +08:00
|
|
|
|
cell = r.cleanTextForPDF(cell)
|
2025-12-03 12:03:42 +08:00
|
|
|
|
|
|
|
|
|
|
// 绘制单元格背景
|
|
|
|
|
|
if fill {
|
|
|
|
|
|
pdf.SetFillColor(250, 250, 235) // 稍深的米色
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pdf.SetFillColor(255, 255, 255)
|
|
|
|
|
|
}
|
|
|
|
|
|
pdf.Rect(currentX, startY, colW, maxCellHeight, "FD")
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制单元格文本(只绘制非空内容)
|
|
|
|
|
|
if cell != "" {
|
|
|
|
|
|
// 计算文本的实际宽度和单元格可用宽度
|
|
|
|
|
|
cellWidth := colW - 4
|
|
|
|
|
|
textWidth := r.getTextWidth(pdf, cell)
|
|
|
|
|
|
|
|
|
|
|
|
var textLines []string
|
|
|
|
|
|
// 只有当文本宽度超过单元格宽度时才换行
|
|
|
|
|
|
if textWidth > cellWidth {
|
2025-12-03 18:02:49 +08:00
|
|
|
|
// 文本需要换行,使用安全的SplitText方法
|
|
|
|
|
|
textLines = r.safeSplitText(pdf, cell, cellWidth)
|
2025-12-03 12:03:42 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 文本不需要换行,单行显示
|
|
|
|
|
|
textLines = []string{cell}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
textHeight := float64(len(textLines)) * lineHt
|
|
|
|
|
|
if textHeight < lineHt {
|
|
|
|
|
|
textHeight = lineHt
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算垂直居中的Y位置
|
|
|
|
|
|
cellCenterY := startY + maxCellHeight/2
|
|
|
|
|
|
textStartY := cellCenterY - textHeight/2
|
|
|
|
|
|
|
|
|
|
|
|
// 设置文本位置(水平左对齐,垂直居中,减少左边距)
|
|
|
|
|
|
pdf.SetXY(currentX+2, textStartY)
|
|
|
|
|
|
// 再次确保颜色为深黑色(防止被其他设置覆盖)
|
|
|
|
|
|
pdf.SetTextColor(0, 0, 0)
|
|
|
|
|
|
// 设置字体,确保颜色不会变淡
|
|
|
|
|
|
r.fontManager.SetFont(pdf, "", 9)
|
|
|
|
|
|
// 再次确保颜色为深黑色(在渲染前最后一次设置)
|
|
|
|
|
|
pdf.SetTextColor(0, 0, 0)
|
|
|
|
|
|
// 安全地渲染文本,使用正常的行高
|
|
|
|
|
|
func() {
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if rec := recover(); rec != nil {
|
2025-12-04 10:35:11 +08:00
|
|
|
|
// 静默处理,不记录日志
|
2025-12-03 12:03:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
// 使用正常的行高,文本已经垂直居中
|
|
|
|
|
|
pdf.MultiCell(cellWidth, lineHt, cell, "", "L", false)
|
|
|
|
|
|
}()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置Y坐标
|
|
|
|
|
|
pdf.SetXY(currentX+colW, startY)
|
|
|
|
|
|
currentX += colW
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 10:35:11 +08:00
|
|
|
|
// 检查maxCellHeight是否为有效数值
|
|
|
|
|
|
if math.IsInf(maxCellHeight, 0) || math.IsNaN(maxCellHeight) {
|
|
|
|
|
|
maxCellHeight = lineHt * 2.0
|
|
|
|
|
|
}
|
|
|
|
|
|
if maxCellHeight <= 0 {
|
|
|
|
|
|
maxCellHeight = lineHt * 2.0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
// 移动到下一行
|
2025-12-04 10:35:11 +08:00
|
|
|
|
nextY := startY + maxCellHeight
|
|
|
|
|
|
if math.IsInf(nextY, 0) || math.IsNaN(nextY) {
|
|
|
|
|
|
nextY = pdf.GetY() + lineHt*2.0
|
|
|
|
|
|
}
|
|
|
|
|
|
pdf.SetXY(15.0, nextY)
|
2025-12-03 12:03:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getTextWidth 获取文本宽度
|
|
|
|
|
|
func (r *DatabaseTableRenderer) getTextWidth(pdf *gofpdf.Fpdf, text string) float64 {
|
|
|
|
|
|
if r.fontManager.IsChineseFontAvailable() {
|
|
|
|
|
|
width := pdf.GetStringWidth(text)
|
|
|
|
|
|
// 如果宽度为0或太小,使用更准确的估算
|
|
|
|
|
|
if width < 0.1 {
|
|
|
|
|
|
return r.estimateTextWidth(text)
|
|
|
|
|
|
}
|
|
|
|
|
|
return width
|
|
|
|
|
|
}
|
|
|
|
|
|
// 估算宽度
|
|
|
|
|
|
return r.estimateTextWidth(text)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// estimateTextWidth 估算文本宽度(处理中英文混合)
|
|
|
|
|
|
func (r *DatabaseTableRenderer) estimateTextWidth(text string) float64 {
|
|
|
|
|
|
charCount := 0.0
|
|
|
|
|
|
for _, r := range text {
|
|
|
|
|
|
// 中文字符通常比英文字符宽
|
|
|
|
|
|
if r >= 0x4E00 && r <= 0x9FFF {
|
|
|
|
|
|
charCount += 1.8 // 中文字符约1.8倍宽度
|
|
|
|
|
|
} else if r >= 0x3400 && r <= 0x4DBF {
|
|
|
|
|
|
charCount += 1.8 // 扩展A
|
|
|
|
|
|
} else if r >= 0x20000 && r <= 0x2A6DF {
|
|
|
|
|
|
charCount += 1.8 // 扩展B
|
|
|
|
|
|
} else {
|
|
|
|
|
|
charCount += 1.0 // 英文字符和数字
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return charCount * 3.0 // 基础宽度3mm
|
|
|
|
|
|
}
|