mirror of
https://github.com/ZeroHawkeye/wordZero.git
synced 2026-04-22 15:47:07 +08:00
feat: Add complex table cell content APIs (paragraphs, nested tables, lists, images) (#70)
* Initial plan * Implement complex table cell content features for issue #27 - Add AddCellParagraph method to add paragraphs to table cells - Add AddCellFormattedParagraph for formatted paragraphs in cells - Add ClearCellParagraphs and GetCellParagraphs methods - Add AddNestedTable method for nested tables in cells - Add AddCellList method with support for various list types - Add AddCellImage, AddCellImageFromFile, AddCellImageFromData methods - Add comprehensive tests for all new features - Add complex_table_demo example demonstrating all features Co-authored-by: ZeroHawkeye <161401688+ZeroHawkeye@users.noreply.github.com> * Remove accidental binary and update .gitignore Co-authored-by: ZeroHawkeye <161401688+ZeroHawkeye@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ZeroHawkeye <161401688+ZeroHawkeye@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,9 @@ examples/output/
|
||||
*/output/
|
||||
output/
|
||||
|
||||
# 编译的示例二进制文件
|
||||
complex_table_demo
|
||||
|
||||
# IDE和编辑器文件
|
||||
.cursor/
|
||||
.vscode/
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
// Package main 演示WordZero复杂表格结构功能
|
||||
// 展示如何在表格单元格中添加段落、列表、嵌套表格和图片
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/ZeroHawkeye/wordZero/pkg/document"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("=== WordZero 复杂表格结构演示 ===")
|
||||
|
||||
// 确保输出目录存在
|
||||
if _, err := os.Stat("examples/output"); os.IsNotExist(err) {
|
||||
os.MkdirAll("examples/output", 0755)
|
||||
}
|
||||
|
||||
// 创建新文档
|
||||
doc := document.New()
|
||||
|
||||
// 添加文档标题
|
||||
title := doc.AddParagraph("WordZero 复杂表格结构演示")
|
||||
title.SetAlignment(document.AlignCenter)
|
||||
doc.AddParagraph("") // 空行
|
||||
|
||||
// 演示1:单元格中添加多个段落
|
||||
fmt.Println("1. 单元格多段落演示...")
|
||||
demonstrateMultipleParagraphs(doc)
|
||||
|
||||
// 演示2:单元格中添加列表
|
||||
fmt.Println("2. 单元格列表演示...")
|
||||
demonstrateCellLists(doc)
|
||||
|
||||
// 演示3:单元格中添加嵌套表格
|
||||
fmt.Println("3. 嵌套表格演示...")
|
||||
demonstrateNestedTable(doc)
|
||||
|
||||
// 演示4:单元格中添加图片
|
||||
fmt.Println("4. 单元格图片演示...")
|
||||
demonstrateCellImages(doc)
|
||||
|
||||
// 演示5:综合复杂表格
|
||||
fmt.Println("5. 综合复杂表格演示...")
|
||||
demonstrateComplexTable(doc)
|
||||
|
||||
// 保存文档
|
||||
outputFile := "examples/output/complex_table_demo.docx"
|
||||
err := doc.Save(outputFile)
|
||||
if err != nil {
|
||||
log.Fatalf("保存文档失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n复杂表格演示文档已保存到: %s\n", outputFile)
|
||||
fmt.Println("=== 演示完成 ===")
|
||||
}
|
||||
|
||||
// demonstrateMultipleParagraphs 演示单元格中添加多个段落
|
||||
func demonstrateMultipleParagraphs(doc *document.Document) {
|
||||
doc.AddParagraph("1. 单元格多段落演示")
|
||||
doc.AddParagraph("")
|
||||
|
||||
// 创建表格
|
||||
config := &document.TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 8000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
log.Printf("创建表格失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置表头
|
||||
table.SetCellText(0, 0, "多段落单元格")
|
||||
table.SetCellText(0, 1, "格式化段落单元格")
|
||||
|
||||
// 在单元格中添加多个段落
|
||||
table.SetCellText(1, 0, "这是第一段内容,介绍了表格的基本概念。")
|
||||
table.AddCellParagraph(1, 0, "这是第二段内容,说明了表格的使用方法。")
|
||||
table.AddCellParagraph(1, 0, "这是第三段内容,总结了表格的优势。")
|
||||
|
||||
// 在另一个单元格中添加格式化段落
|
||||
table.SetCellText(1, 1, "普通文本介绍")
|
||||
table.AddCellFormattedParagraph(1, 1, "粗体重点内容", &document.TextFormat{
|
||||
Bold: true,
|
||||
FontSize: 12,
|
||||
})
|
||||
table.AddCellFormattedParagraph(1, 1, "红色提示文字", &document.TextFormat{
|
||||
FontColor: "FF0000",
|
||||
Italic: true,
|
||||
})
|
||||
table.AddCellFormattedParagraph(1, 1, "大号蓝色标题", &document.TextFormat{
|
||||
FontColor: "0000FF",
|
||||
FontSize: 14,
|
||||
Bold: true,
|
||||
})
|
||||
|
||||
doc.AddParagraph("")
|
||||
fmt.Println(" 多段落单元格演示完成")
|
||||
}
|
||||
|
||||
// demonstrateCellLists 演示单元格中添加列表
|
||||
func demonstrateCellLists(doc *document.Document) {
|
||||
doc.AddParagraph("2. 单元格列表演示")
|
||||
doc.AddParagraph("")
|
||||
|
||||
// 创建表格
|
||||
config := &document.TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 3,
|
||||
Width: 9000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
log.Printf("创建表格失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置表头
|
||||
table.SetCellText(0, 0, "无序列表")
|
||||
table.SetCellText(0, 1, "有序列表")
|
||||
table.SetCellText(0, 2, "罗马数字列表")
|
||||
|
||||
// 添加无序列表
|
||||
bulletList := &document.CellListConfig{
|
||||
Type: document.ListTypeBullet,
|
||||
BulletSymbol: document.BulletTypeDot,
|
||||
Items: []string{
|
||||
"第一个要点",
|
||||
"第二个要点",
|
||||
"第三个要点",
|
||||
},
|
||||
}
|
||||
table.ClearCellParagraphs(1, 0) // 清空默认段落
|
||||
table.AddCellList(1, 0, bulletList)
|
||||
|
||||
// 添加有序列表
|
||||
numberList := &document.CellListConfig{
|
||||
Type: document.ListTypeNumber,
|
||||
Items: []string{
|
||||
"第一步操作",
|
||||
"第二步操作",
|
||||
"第三步操作",
|
||||
},
|
||||
}
|
||||
table.ClearCellParagraphs(1, 1)
|
||||
table.AddCellList(1, 1, numberList)
|
||||
|
||||
// 添加罗马数字列表
|
||||
romanList := &document.CellListConfig{
|
||||
Type: document.ListTypeUpperRoman,
|
||||
Items: []string{
|
||||
"主要内容",
|
||||
"次要内容",
|
||||
"补充内容",
|
||||
},
|
||||
}
|
||||
table.ClearCellParagraphs(1, 2)
|
||||
table.AddCellList(1, 2, romanList)
|
||||
|
||||
doc.AddParagraph("")
|
||||
fmt.Println(" 单元格列表演示完成")
|
||||
}
|
||||
|
||||
// demonstrateNestedTable 演示嵌套表格
|
||||
func demonstrateNestedTable(doc *document.Document) {
|
||||
doc.AddParagraph("3. 嵌套表格演示")
|
||||
doc.AddParagraph("")
|
||||
|
||||
// 创建主表格
|
||||
config := &document.TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 8000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
log.Printf("创建表格失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置表头
|
||||
table.SetCellText(0, 0, "产品信息")
|
||||
table.SetCellText(0, 1, "销售数据")
|
||||
|
||||
// 在第一个单元格添加产品信息嵌套表格
|
||||
productNestedConfig := &document.TableConfig{
|
||||
Rows: 3,
|
||||
Cols: 2,
|
||||
Width: 3500,
|
||||
Data: [][]string{
|
||||
{"属性", "值"},
|
||||
{"名称", "智能手表"},
|
||||
{"型号", "SW-2024"},
|
||||
},
|
||||
}
|
||||
table.ClearCellParagraphs(1, 0)
|
||||
table.AddCellParagraph(1, 0, "产品详细信息:")
|
||||
table.AddNestedTable(1, 0, productNestedConfig)
|
||||
|
||||
// 在第二个单元格添加销售数据嵌套表格
|
||||
salesNestedConfig := &document.TableConfig{
|
||||
Rows: 4,
|
||||
Cols: 2,
|
||||
Width: 3500,
|
||||
Data: [][]string{
|
||||
{"季度", "销量"},
|
||||
{"Q1", "1000"},
|
||||
{"Q2", "1500"},
|
||||
{"Q3", "2000"},
|
||||
},
|
||||
}
|
||||
table.ClearCellParagraphs(1, 1)
|
||||
table.AddCellParagraph(1, 1, "季度销售数据:")
|
||||
table.AddNestedTable(1, 1, salesNestedConfig)
|
||||
|
||||
doc.AddParagraph("")
|
||||
fmt.Println(" 嵌套表格演示完成")
|
||||
}
|
||||
|
||||
// demonstrateCellImages 演示单元格中添加图片
|
||||
func demonstrateCellImages(doc *document.Document) {
|
||||
doc.AddParagraph("4. 单元格图片演示")
|
||||
doc.AddParagraph("")
|
||||
|
||||
// 创建表格
|
||||
config := &document.TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 8000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
log.Printf("创建表格失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置表头
|
||||
table.SetCellText(0, 0, "产品图片")
|
||||
table.SetCellText(0, 1, "产品描述")
|
||||
|
||||
// 在单元格中添加图片
|
||||
imageData := createColorImage(150, 100, color.RGBA{100, 150, 255, 255})
|
||||
table.ClearCellParagraphs(1, 0)
|
||||
_, err = doc.AddCellImageFromData(table, 1, 0, imageData, 40) // 40mm宽度
|
||||
if err != nil {
|
||||
log.Printf("添加图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 在另一个单元格添加描述
|
||||
table.SetCellText(1, 1, "产品名称:智能设备")
|
||||
table.AddCellParagraph(1, 1, "规格:150mm x 100mm")
|
||||
table.AddCellFormattedParagraph(1, 1, "状态:在售", &document.TextFormat{
|
||||
FontColor: "00AA00",
|
||||
Bold: true,
|
||||
})
|
||||
|
||||
doc.AddParagraph("")
|
||||
fmt.Println(" 单元格图片演示完成")
|
||||
}
|
||||
|
||||
// demonstrateComplexTable 演示综合复杂表格
|
||||
func demonstrateComplexTable(doc *document.Document) {
|
||||
doc.AddParagraph("5. 综合复杂表格演示")
|
||||
doc.AddParagraph("")
|
||||
|
||||
// 创建复杂表格
|
||||
config := &document.TableConfig{
|
||||
Rows: 4,
|
||||
Cols: 3,
|
||||
Width: 10000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
log.Printf("创建表格失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 第一行:表头
|
||||
table.SetCellText(0, 0, "项目")
|
||||
table.SetCellText(0, 1, "详细信息")
|
||||
table.SetCellText(0, 2, "备注")
|
||||
|
||||
// 第二行:多段落内容
|
||||
table.SetCellText(1, 0, "公司简介")
|
||||
table.ClearCellParagraphs(1, 1)
|
||||
table.AddCellParagraph(1, 1, "WordZero科技是一家专注于文档处理的技术公司。")
|
||||
table.AddCellFormattedParagraph(1, 1, "成立于2024年", &document.TextFormat{Bold: true})
|
||||
table.AddCellFormattedParagraph(1, 1, "总部位于北京", &document.TextFormat{Italic: true})
|
||||
|
||||
// 添加备注列表
|
||||
noteList := &document.CellListConfig{
|
||||
Type: document.ListTypeBullet,
|
||||
BulletSymbol: document.BulletTypeArrow,
|
||||
Items: []string{"技术驱动", "用户至上", "持续创新"},
|
||||
}
|
||||
table.ClearCellParagraphs(1, 2)
|
||||
table.AddCellList(1, 2, noteList)
|
||||
|
||||
// 第三行:嵌套表格
|
||||
table.SetCellText(2, 0, "产品矩阵")
|
||||
nestedConfig := &document.TableConfig{
|
||||
Rows: 3,
|
||||
Cols: 2,
|
||||
Width: 3000,
|
||||
Data: [][]string{
|
||||
{"产品", "版本"},
|
||||
{"WordZero Core", "v1.0"},
|
||||
{"WordZero Pro", "v2.0"},
|
||||
},
|
||||
}
|
||||
table.ClearCellParagraphs(2, 1)
|
||||
table.AddNestedTable(2, 1, nestedConfig)
|
||||
table.SetCellText(2, 2, "更多产品开发中...")
|
||||
|
||||
// 第四行:图片和描述
|
||||
table.SetCellText(3, 0, "团队展示")
|
||||
|
||||
// 添加团队图片
|
||||
teamImage := createColorImage(120, 80, color.RGBA{150, 200, 150, 255})
|
||||
table.ClearCellParagraphs(3, 1)
|
||||
doc.AddCellImageFromData(table, 3, 1, teamImage, 35)
|
||||
table.AddCellParagraph(3, 1, "专业团队")
|
||||
|
||||
// 添加联系方式
|
||||
contactList := &document.CellListConfig{
|
||||
Type: document.ListTypeNumber,
|
||||
Items: []string{
|
||||
"电话:400-123-4567",
|
||||
"邮箱:contact@wordzero.com",
|
||||
"网站:www.wordzero.com",
|
||||
},
|
||||
}
|
||||
table.ClearCellParagraphs(3, 2)
|
||||
table.AddCellList(3, 2, contactList)
|
||||
|
||||
doc.AddParagraph("")
|
||||
fmt.Println(" 综合复杂表格演示完成")
|
||||
}
|
||||
|
||||
// createColorImage 创建指定颜色的示例图片
|
||||
func createColorImage(width, height int, bgColor color.RGBA) []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// 填充背景色
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
img.Set(x, y, bgColor)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加边框
|
||||
borderColor := color.RGBA{50, 50, 50, 255}
|
||||
for x := 0; x < width; x++ {
|
||||
img.Set(x, 0, borderColor)
|
||||
img.Set(x, height-1, borderColor)
|
||||
}
|
||||
for y := 0; y < height; y++ {
|
||||
img.Set(0, y, borderColor)
|
||||
img.Set(width-1, y, borderColor)
|
||||
}
|
||||
|
||||
// 添加中心十字标记
|
||||
centerX, centerY := width/2, height/2
|
||||
markColor := color.RGBA{0, 0, 0, 255}
|
||||
for x := centerX - 10; x <= centerX+10 && x < width; x++ {
|
||||
if x >= 0 {
|
||||
img.Set(x, centerY, markColor)
|
||||
}
|
||||
}
|
||||
for y := centerY - 10; y <= centerY+10 && y < height; y++ {
|
||||
if y >= 0 {
|
||||
img.Set(centerX, y, markColor)
|
||||
}
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
png.Encode(buf, img)
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -1079,6 +1079,168 @@ func (d *Document) SetImageTitle(imageInfo *ImageInfo, title string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddCellImage 向表格单元格添加图片
|
||||
//
|
||||
// 此方法用于向表格单元格中添加图片,支持从文件路径或二进制数据添加。
|
||||
// 由于图片需要在文档级别管理资源关系,所以此方法必须在Document上调用。
|
||||
//
|
||||
// 参数:
|
||||
// - table: 目标表格
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
// - config: 单元格图片配置
|
||||
//
|
||||
// 返回:
|
||||
// - *ImageInfo: 添加的图片信息
|
||||
// - error: 如果添加失败则返回错误
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// table, _ := doc.AddTable(&document.TableConfig{Rows: 2, Cols: 2, Width: 6000})
|
||||
// imageConfig := &document.CellImageConfig{
|
||||
// FilePath: "logo.png",
|
||||
// Width: 50, // 50mm宽度
|
||||
// KeepAspectRatio: true,
|
||||
// }
|
||||
// imageInfo, err := doc.AddCellImage(table, 0, 0, imageConfig)
|
||||
func (d *Document) AddCellImage(table *Table, row, col int, config *CellImageConfig) (*ImageInfo, error) {
|
||||
if table == nil {
|
||||
return nil, fmt.Errorf("表格不能为空")
|
||||
}
|
||||
|
||||
cell, err := table.GetCell(row, col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var imageData []byte
|
||||
var format ImageFormat
|
||||
var width, height int
|
||||
|
||||
// 从文件或数据获取图片
|
||||
if config.FilePath != "" {
|
||||
// 从文件读取图片
|
||||
imageData, err = os.ReadFile(config.FilePath)
|
||||
if err != nil {
|
||||
Errorf("读取图片文件失败 %s: %v", config.FilePath, err)
|
||||
return nil, fmt.Errorf("读取图片文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 检测图片格式
|
||||
format, err = detectImageFormat(imageData)
|
||||
if err != nil {
|
||||
Errorf("检测图片格式失败 %s: %v", config.FilePath, err)
|
||||
return nil, fmt.Errorf("检测图片格式失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取图片尺寸
|
||||
width, height, err = getImageDimensions(imageData, format)
|
||||
if err != nil {
|
||||
Errorf("获取图片尺寸失败 %s: %v", config.FilePath, err)
|
||||
return nil, fmt.Errorf("获取图片尺寸失败: %v", err)
|
||||
}
|
||||
} else if len(config.Data) > 0 {
|
||||
// 使用提供的二进制数据
|
||||
imageData = config.Data
|
||||
|
||||
if config.Format == "" {
|
||||
// 检测图片格式
|
||||
format, err = detectImageFormat(imageData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检测图片格式失败: %v", err)
|
||||
}
|
||||
} else {
|
||||
format = config.Format
|
||||
}
|
||||
|
||||
// 获取图片尺寸
|
||||
width, height, err = getImageDimensions(imageData, format)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取图片尺寸失败: %v", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("必须提供图片文件路径或二进制数据")
|
||||
}
|
||||
|
||||
// 创建图片配置
|
||||
imageConfig := &ImageConfig{
|
||||
Position: ImagePositionInline,
|
||||
Alignment: AlignCenter,
|
||||
AltText: config.AltText,
|
||||
Title: config.Title,
|
||||
}
|
||||
|
||||
if config.Width > 0 || config.Height > 0 {
|
||||
imageConfig.Size = &ImageSize{
|
||||
Width: config.Width,
|
||||
Height: config.Height,
|
||||
KeepAspectRatio: config.KeepAspectRatio,
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Document的方法添加图片资源,但不添加到文档主体
|
||||
fileName := "cell_image.png"
|
||||
if config.FilePath != "" {
|
||||
fileName = config.FilePath
|
||||
}
|
||||
|
||||
imageInfo, err := d.AddImageFromDataWithoutElement(imageData, fileName, format, width, height, imageConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建包含图片的段落并添加到单元格
|
||||
paragraph := d.createImageParagraph(imageInfo)
|
||||
cell.Paragraphs = append(cell.Paragraphs, *paragraph)
|
||||
|
||||
Infof("向表格单元格(%d,%d)添加图片成功: ID=%s", row, col, imageInfo.ID)
|
||||
return imageInfo, nil
|
||||
}
|
||||
|
||||
// AddCellImageFromFile 从文件向表格单元格添加图片(便捷方法)
|
||||
//
|
||||
// 此方法是AddCellImage的便捷封装,直接从文件路径添加图片。
|
||||
//
|
||||
// 参数:
|
||||
// - table: 目标表格
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
// - filePath: 图片文件路径
|
||||
// - widthMM: 图片宽度(毫米),0表示使用原始尺寸
|
||||
//
|
||||
// 返回:
|
||||
// - *ImageInfo: 添加的图片信息
|
||||
// - error: 如果添加失败则返回错误
|
||||
func (d *Document) AddCellImageFromFile(table *Table, row, col int, filePath string, widthMM float64) (*ImageInfo, error) {
|
||||
return d.AddCellImage(table, row, col, &CellImageConfig{
|
||||
FilePath: filePath,
|
||||
Width: widthMM,
|
||||
KeepAspectRatio: true,
|
||||
})
|
||||
}
|
||||
|
||||
// AddCellImageFromData 从二进制数据向表格单元格添加图片(便捷方法)
|
||||
//
|
||||
// 此方法是AddCellImage的便捷封装,直接从二进制数据添加图片。
|
||||
//
|
||||
// 参数:
|
||||
// - table: 目标表格
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
// - data: 图片二进制数据
|
||||
// - widthMM: 图片宽度(毫米),0表示使用原始尺寸
|
||||
//
|
||||
// 返回:
|
||||
// - *ImageInfo: 添加的图片信息
|
||||
// - error: 如果添加失败则返回错误
|
||||
func (d *Document) AddCellImageFromData(table *Table, row, col int, data []byte, widthMM float64) (*ImageInfo, error) {
|
||||
return d.AddCellImage(table, row, col, &CellImageConfig{
|
||||
Data: data,
|
||||
Width: widthMM,
|
||||
KeepAspectRatio: true,
|
||||
})
|
||||
}
|
||||
|
||||
// SetImageAlignment 设置图片对齐方式
|
||||
//
|
||||
// 此方法用于设置嵌入式图片(ImagePositionInline)的对齐方式。
|
||||
|
||||
@@ -2643,3 +2643,426 @@ func (t *Table) FindCellsByText(searchText string, exactMatch bool) ([]*CellInfo
|
||||
return strings.Contains(text, searchText)
|
||||
})
|
||||
}
|
||||
|
||||
// ============== 单元格复杂内容功能 ==============
|
||||
// 以下方法支持向表格单元格中添加段落、图片、列表、嵌套表格等复杂内容
|
||||
|
||||
// AddCellParagraph 向单元格添加段落
|
||||
// 参数:
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
// - text: 段落文本内容
|
||||
//
|
||||
// 返回:
|
||||
// - *Paragraph: 新添加的段落对象
|
||||
// - error: 如果索引无效则返回错误
|
||||
func (t *Table) AddCellParagraph(row, col int, text string) (*Paragraph, error) {
|
||||
cell, err := t.GetCell(row, col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建新段落
|
||||
para := &Paragraph{
|
||||
Runs: []Run{
|
||||
{
|
||||
Text: Text{
|
||||
Content: text,
|
||||
Space: "preserve",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 添加到单元格
|
||||
cell.Paragraphs = append(cell.Paragraphs, *para)
|
||||
|
||||
Info(fmt.Sprintf("向单元格(%d,%d)添加段落成功", row, col))
|
||||
return &cell.Paragraphs[len(cell.Paragraphs)-1], nil
|
||||
}
|
||||
|
||||
// AddCellFormattedParagraph 向单元格添加格式化段落
|
||||
// 参数:
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
// - text: 段落文本内容
|
||||
// - format: 文本格式配置
|
||||
//
|
||||
// 返回:
|
||||
// - *Paragraph: 新添加的段落对象
|
||||
// - error: 如果索引无效则返回错误
|
||||
func (t *Table) AddCellFormattedParagraph(row, col int, text string, format *TextFormat) (*Paragraph, error) {
|
||||
cell, err := t.GetCell(row, col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建运行属性
|
||||
runProps := &RunProperties{}
|
||||
|
||||
if format != nil {
|
||||
if format.FontFamily != "" {
|
||||
runProps.FontFamily = &FontFamily{
|
||||
ASCII: format.FontFamily,
|
||||
HAnsi: format.FontFamily,
|
||||
EastAsia: format.FontFamily,
|
||||
CS: format.FontFamily,
|
||||
}
|
||||
}
|
||||
|
||||
if format.Bold {
|
||||
runProps.Bold = &Bold{}
|
||||
}
|
||||
|
||||
if format.Italic {
|
||||
runProps.Italic = &Italic{}
|
||||
}
|
||||
|
||||
if format.FontColor != "" {
|
||||
color := strings.TrimPrefix(format.FontColor, "#")
|
||||
runProps.Color = &Color{Val: color}
|
||||
}
|
||||
|
||||
if format.FontSize > 0 {
|
||||
runProps.FontSize = &FontSize{Val: fmt.Sprintf("%d", format.FontSize*2)}
|
||||
}
|
||||
|
||||
if format.Underline {
|
||||
runProps.Underline = &Underline{Val: "single"}
|
||||
}
|
||||
|
||||
if format.Strike {
|
||||
runProps.Strike = &Strike{}
|
||||
}
|
||||
|
||||
if format.Highlight != "" {
|
||||
runProps.Highlight = &Highlight{Val: format.Highlight}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新段落
|
||||
para := &Paragraph{
|
||||
Runs: []Run{
|
||||
{
|
||||
Properties: runProps,
|
||||
Text: Text{
|
||||
Content: text,
|
||||
Space: "preserve",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 添加到单元格
|
||||
cell.Paragraphs = append(cell.Paragraphs, *para)
|
||||
|
||||
Info(fmt.Sprintf("向单元格(%d,%d)添加格式化段落成功", row, col))
|
||||
return &cell.Paragraphs[len(cell.Paragraphs)-1], nil
|
||||
}
|
||||
|
||||
// ClearCellParagraphs 清空单元格中的所有段落,只保留一个空段落
|
||||
// 参数:
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
//
|
||||
// 返回:
|
||||
// - error: 如果索引无效则返回错误
|
||||
func (t *Table) ClearCellParagraphs(row, col int) error {
|
||||
cell, err := t.GetCell(row, col)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清空段落,只保留一个空段落(OOXML规范要求单元格至少有一个段落)
|
||||
cell.Paragraphs = []Paragraph{
|
||||
{
|
||||
Runs: []Run{
|
||||
{
|
||||
Text: Text{Content: ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Info(fmt.Sprintf("清空单元格(%d,%d)段落成功", row, col))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCellParagraphs 获取单元格中的所有段落
|
||||
// 参数:
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
//
|
||||
// 返回:
|
||||
// - []Paragraph: 单元格中的所有段落
|
||||
// - error: 如果索引无效则返回错误
|
||||
func (t *Table) GetCellParagraphs(row, col int) ([]Paragraph, error) {
|
||||
cell, err := t.GetCell(row, col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cell.Paragraphs, nil
|
||||
}
|
||||
|
||||
// AddNestedTable 向单元格添加嵌套表格
|
||||
// 参数:
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
// - config: 嵌套表格的配置
|
||||
//
|
||||
// 返回:
|
||||
// - *Table: 新创建的嵌套表格对象
|
||||
// - error: 如果索引无效或配置无效则返回错误
|
||||
func (t *Table) AddNestedTable(row, col int, config *TableConfig) (*Table, error) {
|
||||
cell, err := t.GetCell(row, col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config.Rows <= 0 || config.Cols <= 0 {
|
||||
Error("嵌套表格行数和列数必须大于0")
|
||||
return nil, NewValidationError("TableConfig", "", "嵌套表格行数和列数必须大于0")
|
||||
}
|
||||
|
||||
// 创建嵌套表格
|
||||
nestedTable := &Table{
|
||||
Properties: &TableProperties{
|
||||
TableW: &TableWidth{
|
||||
W: fmt.Sprintf("%d", config.Width),
|
||||
Type: "dxa",
|
||||
},
|
||||
TableJc: &TableJc{
|
||||
Val: "center",
|
||||
},
|
||||
TableLook: &TableLook{
|
||||
Val: "04A0",
|
||||
FirstRow: "1",
|
||||
LastRow: "0",
|
||||
FirstCol: "1",
|
||||
LastCol: "0",
|
||||
NoHBand: "0",
|
||||
NoVBand: "1",
|
||||
},
|
||||
TableBorders: &TableBorders{
|
||||
Top: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
|
||||
Left: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
|
||||
Bottom: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
|
||||
Right: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
|
||||
InsideH: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
|
||||
InsideV: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
|
||||
},
|
||||
TableLayout: &TableLayoutType{
|
||||
Type: "autofit",
|
||||
},
|
||||
TableCellMar: &TableCellMargins{
|
||||
Left: &TableCellSpace{W: "108", Type: "dxa"},
|
||||
Right: &TableCellSpace{W: "108", Type: "dxa"},
|
||||
},
|
||||
},
|
||||
Grid: &TableGrid{},
|
||||
Rows: make([]TableRow, 0, config.Rows),
|
||||
}
|
||||
|
||||
// 设置列宽
|
||||
colWidths := config.ColWidths
|
||||
if len(colWidths) == 0 {
|
||||
avgWidth := config.Width / config.Cols
|
||||
colWidths = make([]int, config.Cols)
|
||||
for i := range colWidths {
|
||||
colWidths[i] = avgWidth
|
||||
}
|
||||
} else if len(colWidths) != config.Cols {
|
||||
Error("嵌套表格列宽数量与列数不匹配")
|
||||
return nil, NewValidationError("TableConfig.ColWidths", "", "列宽数量与列数不匹配")
|
||||
}
|
||||
|
||||
// 创建表格网格
|
||||
for _, width := range colWidths {
|
||||
nestedTable.Grid.Cols = append(nestedTable.Grid.Cols, TableGridCol{
|
||||
W: fmt.Sprintf("%d", width),
|
||||
})
|
||||
}
|
||||
|
||||
// 创建表格行和单元格
|
||||
for i := 0; i < config.Rows; i++ {
|
||||
tableRow := TableRow{
|
||||
Cells: make([]TableCell, 0, config.Cols),
|
||||
}
|
||||
|
||||
for j := 0; j < config.Cols; j++ {
|
||||
tableCell := TableCell{
|
||||
Properties: &TableCellProperties{
|
||||
TableCellW: &TableCellW{
|
||||
W: fmt.Sprintf("%d", colWidths[j]),
|
||||
Type: "dxa",
|
||||
},
|
||||
VAlign: &VAlign{
|
||||
Val: "center",
|
||||
},
|
||||
},
|
||||
Paragraphs: []Paragraph{
|
||||
{
|
||||
Runs: []Run{
|
||||
{
|
||||
Text: Text{Content: ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 如果有初始数据,设置单元格内容
|
||||
if config.Data != nil && i < len(config.Data) && j < len(config.Data[i]) {
|
||||
tableCell.Paragraphs[0].Runs[0].Text.Content = config.Data[i][j]
|
||||
}
|
||||
|
||||
tableRow.Cells = append(tableRow.Cells, tableCell)
|
||||
}
|
||||
|
||||
nestedTable.Rows = append(nestedTable.Rows, tableRow)
|
||||
}
|
||||
|
||||
// 添加到单元格的嵌套表格列表
|
||||
cell.Tables = append(cell.Tables, *nestedTable)
|
||||
|
||||
Info(fmt.Sprintf("向单元格(%d,%d)添加嵌套表格成功:%d行 x %d列", row, col, config.Rows, config.Cols))
|
||||
return &cell.Tables[len(cell.Tables)-1], nil
|
||||
}
|
||||
|
||||
// GetNestedTables 获取单元格中的所有嵌套表格
|
||||
// 参数:
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
//
|
||||
// 返回:
|
||||
// - []Table: 单元格中的所有嵌套表格
|
||||
// - error: 如果索引无效则返回错误
|
||||
func (t *Table) GetNestedTables(row, col int) ([]Table, error) {
|
||||
cell, err := t.GetCell(row, col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cell.Tables, nil
|
||||
}
|
||||
|
||||
// CellListConfig 单元格列表配置
|
||||
type CellListConfig struct {
|
||||
Type ListType // 列表类型
|
||||
BulletSymbol BulletType // 项目符号(仅用于无序列表)
|
||||
Items []string // 列表项内容
|
||||
}
|
||||
|
||||
// AddCellList 向单元格添加列表
|
||||
// 参数:
|
||||
// - row: 行索引(从0开始)
|
||||
// - col: 列索引(从0开始)
|
||||
// - config: 列表配置
|
||||
//
|
||||
// 返回:
|
||||
// - error: 如果索引无效则返回错误
|
||||
func (t *Table) AddCellList(row, col int, config *CellListConfig) error {
|
||||
cell, err := t.GetCell(row, col)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config == nil || len(config.Items) == 0 {
|
||||
return NewValidationError("CellListConfig", "", "列表配置不能为空且必须包含列表项")
|
||||
}
|
||||
|
||||
// 根据列表类型确定前缀
|
||||
for i, item := range config.Items {
|
||||
var prefix string
|
||||
switch config.Type {
|
||||
case ListTypeBullet:
|
||||
// 使用项目符号
|
||||
bulletSymbol := config.BulletSymbol
|
||||
if bulletSymbol == "" {
|
||||
bulletSymbol = BulletTypeDot
|
||||
}
|
||||
prefix = string(bulletSymbol) + " "
|
||||
case ListTypeNumber, ListTypeDecimal:
|
||||
// 使用数字编号
|
||||
prefix = fmt.Sprintf("%d. ", i+1)
|
||||
case ListTypeLowerLetter:
|
||||
// 使用小写字母
|
||||
prefix = fmt.Sprintf("%c. ", 'a'+i)
|
||||
case ListTypeUpperLetter:
|
||||
// 使用大写字母
|
||||
prefix = fmt.Sprintf("%c. ", 'A'+i)
|
||||
case ListTypeLowerRoman:
|
||||
// 使用小写罗马数字
|
||||
prefix = fmt.Sprintf("%s. ", toRomanLower(i+1))
|
||||
case ListTypeUpperRoman:
|
||||
// 使用大写罗马数字
|
||||
prefix = fmt.Sprintf("%s. ", toRomanUpper(i+1))
|
||||
default:
|
||||
// 默认使用项目符号
|
||||
prefix = string(BulletTypeDot) + " "
|
||||
}
|
||||
|
||||
// 创建列表项段落
|
||||
para := Paragraph{
|
||||
Runs: []Run{
|
||||
{
|
||||
Text: Text{
|
||||
Content: prefix + item,
|
||||
Space: "preserve",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 添加到单元格
|
||||
cell.Paragraphs = append(cell.Paragraphs, para)
|
||||
}
|
||||
|
||||
Info(fmt.Sprintf("向单元格(%d,%d)添加列表成功:%d个列表项", row, col, len(config.Items)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// toRomanLower 将数字转换为小写罗马数字
|
||||
func toRomanLower(num int) string {
|
||||
return strings.ToLower(toRomanUpper(num))
|
||||
}
|
||||
|
||||
// toRomanUpper 将数字转换为大写罗马数字
|
||||
func toRomanUpper(num int) string {
|
||||
values := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}
|
||||
symbols := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}
|
||||
|
||||
if num <= 0 || num > 3999 {
|
||||
return fmt.Sprintf("%d", num)
|
||||
}
|
||||
|
||||
result := ""
|
||||
for i, value := range values {
|
||||
for num >= value {
|
||||
result += symbols[i]
|
||||
num -= value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CellImageConfig 单元格图片配置
|
||||
type CellImageConfig struct {
|
||||
// 图片来源 - 文件路径
|
||||
FilePath string
|
||||
// 图片来源 - 二进制数据
|
||||
Data []byte
|
||||
// 图片格式(当使用Data时需要指定)
|
||||
Format ImageFormat
|
||||
// 图片宽度(毫米),0表示自动
|
||||
Width float64
|
||||
// 图片高度(毫米),0表示自动
|
||||
Height float64
|
||||
// 是否保持宽高比
|
||||
KeepAspectRatio bool
|
||||
// 图片替代文字
|
||||
AltText string
|
||||
// 图片标题
|
||||
Title string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,727 @@
|
||||
package document
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// createCellTestImage 创建测试用的PNG图片数据
|
||||
func createCellTestImage(width, height int) []byte {
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// 填充红色背景
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
img.Set(x, y, color.RGBA{255, 100, 100, 255})
|
||||
}
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
png.Encode(buf, img)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// TestAddCellParagraph 测试向单元格添加段落
|
||||
func TestAddCellParagraph(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 4000,
|
||||
}
|
||||
|
||||
table, err := doc.CreateTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试添加段落
|
||||
para, err := table.AddCellParagraph(0, 0, "第一段内容")
|
||||
if err != nil {
|
||||
t.Errorf("添加段落失败: %v", err)
|
||||
}
|
||||
if para == nil {
|
||||
t.Error("返回的段落不应为空")
|
||||
}
|
||||
|
||||
// 添加第二个段落
|
||||
para2, err := table.AddCellParagraph(0, 0, "第二段内容")
|
||||
if err != nil {
|
||||
t.Errorf("添加第二段落失败: %v", err)
|
||||
}
|
||||
if para2 == nil {
|
||||
t.Error("返回的第二段落不应为空")
|
||||
}
|
||||
|
||||
// 验证段落数量
|
||||
paragraphs, err := table.GetCellParagraphs(0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("获取段落失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始有一个空段落,加上两个新段落
|
||||
if len(paragraphs) < 3 {
|
||||
t.Errorf("期望至少3个段落,实际%d", len(paragraphs))
|
||||
}
|
||||
|
||||
// 测试无效索引
|
||||
_, err = table.AddCellParagraph(10, 10, "无效")
|
||||
if err == nil {
|
||||
t.Error("期望无效索引失败,但成功了")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddCellFormattedParagraph 测试向单元格添加格式化段落
|
||||
func TestAddCellFormattedParagraph(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 4000,
|
||||
}
|
||||
|
||||
table, err := doc.CreateTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试添加格式化段落
|
||||
format := &TextFormat{
|
||||
Bold: true,
|
||||
Italic: true,
|
||||
FontSize: 14,
|
||||
FontColor: "FF0000",
|
||||
FontFamily: "Arial",
|
||||
Underline: true,
|
||||
}
|
||||
|
||||
para, err := table.AddCellFormattedParagraph(0, 0, "格式化内容", format)
|
||||
if err != nil {
|
||||
t.Errorf("添加格式化段落失败: %v", err)
|
||||
}
|
||||
if para == nil {
|
||||
t.Error("返回的段落不应为空")
|
||||
}
|
||||
|
||||
// 验证格式
|
||||
if len(para.Runs) == 0 {
|
||||
t.Error("段落应包含至少一个Run")
|
||||
}
|
||||
|
||||
run := para.Runs[0]
|
||||
if run.Properties == nil {
|
||||
t.Error("Run应有属性")
|
||||
} else {
|
||||
if run.Properties.Bold == nil {
|
||||
t.Error("期望粗体属性")
|
||||
}
|
||||
if run.Properties.Italic == nil {
|
||||
t.Error("期望斜体属性")
|
||||
}
|
||||
if run.Properties.Underline == nil {
|
||||
t.Error("期望下划线属性")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClearCellParagraphs 测试清空单元格段落
|
||||
func TestClearCellParagraphs(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 4000,
|
||||
Data: [][]string{
|
||||
{"A1", "B1"},
|
||||
{"A2", "B2"},
|
||||
},
|
||||
}
|
||||
|
||||
table, err := doc.CreateTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加多个段落
|
||||
table.AddCellParagraph(0, 0, "段落1")
|
||||
table.AddCellParagraph(0, 0, "段落2")
|
||||
|
||||
// 清空段落
|
||||
err = table.ClearCellParagraphs(0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("清空段落失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证清空后只有一个空段落
|
||||
paragraphs, err := table.GetCellParagraphs(0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("获取段落失败: %v", err)
|
||||
}
|
||||
|
||||
if len(paragraphs) != 1 {
|
||||
t.Errorf("期望清空后只有1个段落,实际%d", len(paragraphs))
|
||||
}
|
||||
|
||||
// 测试无效索引
|
||||
err = table.ClearCellParagraphs(10, 10)
|
||||
if err == nil {
|
||||
t.Error("期望无效索引失败,但成功了")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddNestedTable 测试向单元格添加嵌套表格
|
||||
func TestAddNestedTable(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
// 创建主表格
|
||||
mainConfig := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 8000,
|
||||
}
|
||||
|
||||
mainTable, err := doc.CreateTable(mainConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("创建主表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建嵌套表格配置
|
||||
nestedConfig := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 3,
|
||||
Width: 3000,
|
||||
Data: [][]string{
|
||||
{"嵌套1", "嵌套2", "嵌套3"},
|
||||
{"数据1", "数据2", "数据3"},
|
||||
},
|
||||
}
|
||||
|
||||
// 添加嵌套表格
|
||||
nestedTable, err := mainTable.AddNestedTable(0, 0, nestedConfig)
|
||||
if err != nil {
|
||||
t.Errorf("添加嵌套表格失败: %v", err)
|
||||
}
|
||||
if nestedTable == nil {
|
||||
t.Error("返回的嵌套表格不应为空")
|
||||
}
|
||||
|
||||
// 验证嵌套表格结构
|
||||
if nestedTable.GetRowCount() != 2 {
|
||||
t.Errorf("期望嵌套表格2行,实际%d", nestedTable.GetRowCount())
|
||||
}
|
||||
if nestedTable.GetColumnCount() != 3 {
|
||||
t.Errorf("期望嵌套表格3列,实际%d", nestedTable.GetColumnCount())
|
||||
}
|
||||
|
||||
// 验证嵌套表格内容
|
||||
cellText, err := nestedTable.GetCellText(0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("获取嵌套表格单元格内容失败: %v", err)
|
||||
}
|
||||
if cellText != "嵌套1" {
|
||||
t.Errorf("期望嵌套表格内容'嵌套1',实际'%s'", cellText)
|
||||
}
|
||||
|
||||
// 获取嵌套表格列表
|
||||
nestedTables, err := mainTable.GetNestedTables(0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("获取嵌套表格列表失败: %v", err)
|
||||
}
|
||||
if len(nestedTables) != 1 {
|
||||
t.Errorf("期望1个嵌套表格,实际%d", len(nestedTables))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddNestedTableInvalidConfig 测试嵌套表格的无效配置
|
||||
func TestAddNestedTableInvalidConfig(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
mainConfig := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 4000,
|
||||
}
|
||||
|
||||
mainTable, err := doc.CreateTable(mainConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("创建主表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试无效的行列数
|
||||
_, err = mainTable.AddNestedTable(0, 0, &TableConfig{Rows: 0, Cols: 2, Width: 2000})
|
||||
if err == nil {
|
||||
t.Error("期望行数为0时失败,但成功了")
|
||||
}
|
||||
|
||||
_, err = mainTable.AddNestedTable(0, 0, &TableConfig{Rows: 2, Cols: 0, Width: 2000})
|
||||
if err == nil {
|
||||
t.Error("期望列数为0时失败,但成功了")
|
||||
}
|
||||
|
||||
// 测试无效的单元格索引
|
||||
_, err = mainTable.AddNestedTable(10, 10, &TableConfig{Rows: 2, Cols: 2, Width: 2000})
|
||||
if err == nil {
|
||||
t.Error("期望无效索引失败,但成功了")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddCellList 测试向单元格添加列表
|
||||
func TestAddCellList(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 3,
|
||||
Cols: 2,
|
||||
Width: 6000,
|
||||
}
|
||||
|
||||
table, err := doc.CreateTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试添加无序列表
|
||||
bulletListConfig := &CellListConfig{
|
||||
Type: ListTypeBullet,
|
||||
BulletSymbol: BulletTypeDot,
|
||||
Items: []string{"项目一", "项目二", "项目三"},
|
||||
}
|
||||
|
||||
err = table.AddCellList(0, 0, bulletListConfig)
|
||||
if err != nil {
|
||||
t.Errorf("添加无序列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证列表项数量
|
||||
paragraphs, err := table.GetCellParagraphs(0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("获取段落失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始有一个空段落,加上3个列表项
|
||||
expectedCount := 1 + 3
|
||||
if len(paragraphs) != expectedCount {
|
||||
t.Errorf("期望%d个段落,实际%d", expectedCount, len(paragraphs))
|
||||
}
|
||||
|
||||
// 测试添加有序列表
|
||||
numberListConfig := &CellListConfig{
|
||||
Type: ListTypeNumber,
|
||||
Items: []string{"第一步", "第二步", "第三步"},
|
||||
}
|
||||
|
||||
err = table.AddCellList(1, 0, numberListConfig)
|
||||
if err != nil {
|
||||
t.Errorf("添加有序列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试添加小写字母列表
|
||||
letterListConfig := &CellListConfig{
|
||||
Type: ListTypeLowerLetter,
|
||||
Items: []string{"选项a", "选项b"},
|
||||
}
|
||||
|
||||
err = table.AddCellList(2, 0, letterListConfig)
|
||||
if err != nil {
|
||||
t.Errorf("添加字母列表失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddCellListInvalidConfig 测试列表的无效配置
|
||||
func TestAddCellListInvalidConfig(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 4000,
|
||||
}
|
||||
|
||||
table, err := doc.CreateTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试空配置
|
||||
err = table.AddCellList(0, 0, nil)
|
||||
if err == nil {
|
||||
t.Error("期望空配置失败,但成功了")
|
||||
}
|
||||
|
||||
// 测试空列表项
|
||||
err = table.AddCellList(0, 0, &CellListConfig{Type: ListTypeBullet, Items: []string{}})
|
||||
if err == nil {
|
||||
t.Error("期望空列表项失败,但成功了")
|
||||
}
|
||||
|
||||
// 测试无效索引
|
||||
err = table.AddCellList(10, 10, &CellListConfig{Type: ListTypeBullet, Items: []string{"测试"}})
|
||||
if err == nil {
|
||||
t.Error("期望无效索引失败,但成功了")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddCellImage 测试向单元格添加图片
|
||||
func TestAddCellImage(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 6000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建测试图片数据
|
||||
imageData := createCellTestImage(100, 100)
|
||||
|
||||
// 测试从数据添加图片
|
||||
imageInfo, err := doc.AddCellImageFromData(table, 0, 0, imageData, 30)
|
||||
if err != nil {
|
||||
t.Errorf("添加图片失败: %v", err)
|
||||
}
|
||||
if imageInfo == nil {
|
||||
t.Error("返回的图片信息不应为空")
|
||||
}
|
||||
|
||||
// 验证图片ID不为空
|
||||
if imageInfo.ID == "" {
|
||||
t.Error("图片ID不应为空")
|
||||
}
|
||||
|
||||
// 验证关系ID不为空
|
||||
if imageInfo.RelationID == "" {
|
||||
t.Error("关系ID不应为空")
|
||||
}
|
||||
|
||||
// 验证单元格段落包含图片
|
||||
paragraphs, err := table.GetCellParagraphs(0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("获取段落失败: %v", err)
|
||||
}
|
||||
|
||||
hasImage := false
|
||||
for _, para := range paragraphs {
|
||||
for _, run := range para.Runs {
|
||||
if run.Drawing != nil {
|
||||
hasImage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasImage {
|
||||
t.Error("单元格应包含图片")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddCellImageWithConfig 测试使用配置添加图片
|
||||
func TestAddCellImageWithConfig(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 6000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建测试图片数据
|
||||
imageData := createCellTestImage(200, 150)
|
||||
|
||||
// 使用完整配置添加图片
|
||||
imageConfig := &CellImageConfig{
|
||||
Data: imageData,
|
||||
Width: 50,
|
||||
Height: 40,
|
||||
KeepAspectRatio: false,
|
||||
AltText: "测试图片",
|
||||
Title: "单元格图片",
|
||||
}
|
||||
|
||||
imageInfo, err := doc.AddCellImage(table, 0, 0, imageConfig)
|
||||
if err != nil {
|
||||
t.Errorf("添加图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证图片配置
|
||||
if imageInfo.Config == nil {
|
||||
t.Error("图片配置不应为空")
|
||||
} else {
|
||||
if imageInfo.Config.AltText != "测试图片" {
|
||||
t.Errorf("期望替代文字'测试图片',实际'%s'", imageInfo.Config.AltText)
|
||||
}
|
||||
if imageInfo.Config.Title != "单元格图片" {
|
||||
t.Errorf("期望标题'单元格图片',实际'%s'", imageInfo.Config.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddCellImageInvalidCases 测试添加图片的无效情况
|
||||
func TestAddCellImageInvalidCases(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 4000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试空表格
|
||||
_, err = doc.AddCellImage(nil, 0, 0, &CellImageConfig{Data: createCellTestImage(100, 100)})
|
||||
if err == nil {
|
||||
t.Error("期望空表格失败,但成功了")
|
||||
}
|
||||
|
||||
// 测试无效索引
|
||||
_, err = doc.AddCellImage(table, 10, 10, &CellImageConfig{Data: createCellTestImage(100, 100)})
|
||||
if err == nil {
|
||||
t.Error("期望无效索引失败,但成功了")
|
||||
}
|
||||
|
||||
// 测试无数据配置
|
||||
_, err = doc.AddCellImage(table, 0, 0, &CellImageConfig{})
|
||||
if err == nil {
|
||||
t.Error("期望无数据配置失败,但成功了")
|
||||
}
|
||||
}
|
||||
|
||||
// TestComplexTableStructure 测试复杂表格结构
|
||||
func TestComplexTableStructure(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
// 创建主表格
|
||||
mainConfig := &TableConfig{
|
||||
Rows: 3,
|
||||
Cols: 3,
|
||||
Width: 9000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(mainConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 第一个单元格:添加多个段落
|
||||
table.AddCellParagraph(0, 0, "第一段")
|
||||
table.AddCellFormattedParagraph(0, 0, "格式化段落", &TextFormat{Bold: true})
|
||||
|
||||
// 第二个单元格:添加列表
|
||||
listConfig := &CellListConfig{
|
||||
Type: ListTypeBullet,
|
||||
BulletSymbol: BulletTypeDot,
|
||||
Items: []string{"列表项1", "列表项2"},
|
||||
}
|
||||
table.AddCellList(0, 1, listConfig)
|
||||
|
||||
// 第三个单元格:添加嵌套表格
|
||||
nestedConfig := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 2500,
|
||||
Data: [][]string{
|
||||
{"A", "B"},
|
||||
{"C", "D"},
|
||||
},
|
||||
}
|
||||
table.AddNestedTable(0, 2, nestedConfig)
|
||||
|
||||
// 第四个单元格:添加图片
|
||||
imageData := createCellTestImage(50, 50)
|
||||
doc.AddCellImageFromData(table, 1, 0, imageData, 20)
|
||||
|
||||
// 验证复杂结构
|
||||
paragraphs00, _ := table.GetCellParagraphs(0, 0)
|
||||
if len(paragraphs00) < 3 { // 初始1个 + 添加2个
|
||||
t.Errorf("单元格(0,0)应至少有3个段落,实际%d", len(paragraphs00))
|
||||
}
|
||||
|
||||
paragraphs01, _ := table.GetCellParagraphs(0, 1)
|
||||
if len(paragraphs01) < 3 { // 初始1个 + 列表2项
|
||||
t.Errorf("单元格(0,1)应至少有3个段落,实际%d", len(paragraphs01))
|
||||
}
|
||||
|
||||
nestedTables, _ := table.GetNestedTables(0, 2)
|
||||
if len(nestedTables) != 1 {
|
||||
t.Errorf("单元格(0,2)应有1个嵌套表格,实际%d", len(nestedTables))
|
||||
}
|
||||
|
||||
paragraphs10, _ := table.GetCellParagraphs(1, 0)
|
||||
hasImage := false
|
||||
for _, para := range paragraphs10 {
|
||||
for _, run := range para.Runs {
|
||||
if run.Drawing != nil {
|
||||
hasImage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasImage {
|
||||
t.Error("单元格(1,0)应包含图片")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSaveComplexTable 测试保存复杂表格
|
||||
func TestSaveComplexTable(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
// 创建表格
|
||||
config := &TableConfig{
|
||||
Rows: 3,
|
||||
Cols: 2,
|
||||
Width: 6000,
|
||||
}
|
||||
|
||||
table, err := doc.AddTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加复杂内容
|
||||
table.AddCellParagraph(0, 0, "复杂表格测试")
|
||||
table.AddCellFormattedParagraph(0, 0, "粗体文本", &TextFormat{Bold: true})
|
||||
|
||||
listConfig := &CellListConfig{
|
||||
Type: ListTypeNumber,
|
||||
Items: []string{"第一项", "第二项"},
|
||||
}
|
||||
table.AddCellList(0, 1, listConfig)
|
||||
|
||||
nestedConfig := &TableConfig{
|
||||
Rows: 2,
|
||||
Cols: 2,
|
||||
Width: 2000,
|
||||
Data: [][]string{
|
||||
{"X", "Y"},
|
||||
{"Z", "W"},
|
||||
},
|
||||
}
|
||||
table.AddNestedTable(1, 0, nestedConfig)
|
||||
|
||||
// 添加图片
|
||||
imageData := createCellTestImage(80, 60)
|
||||
doc.AddCellImageFromData(table, 1, 1, imageData, 25)
|
||||
|
||||
// 保存并验证
|
||||
outputDir := "test_output"
|
||||
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
|
||||
os.MkdirAll(outputDir, 0755)
|
||||
}
|
||||
|
||||
outputFile := outputDir + "/complex_table_test.docx"
|
||||
err = doc.Save(outputFile)
|
||||
if err != nil {
|
||||
t.Errorf("保存文档失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证文件存在
|
||||
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
||||
t.Error("输出文件不存在")
|
||||
}
|
||||
|
||||
// 清理
|
||||
defer os.RemoveAll(outputDir)
|
||||
}
|
||||
|
||||
// TestRomanNumerals 测试罗马数字转换
|
||||
func TestRomanNumerals(t *testing.T) {
|
||||
testCases := []struct {
|
||||
num int
|
||||
expected string
|
||||
}{
|
||||
{1, "I"},
|
||||
{2, "II"},
|
||||
{3, "III"},
|
||||
{4, "IV"},
|
||||
{5, "V"},
|
||||
{9, "IX"},
|
||||
{10, "X"},
|
||||
{40, "XL"},
|
||||
{50, "L"},
|
||||
{90, "XC"},
|
||||
{100, "C"},
|
||||
{400, "CD"},
|
||||
{500, "D"},
|
||||
{900, "CM"},
|
||||
{1000, "M"},
|
||||
{1999, "MCMXCIX"},
|
||||
{2024, "MMXXIV"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := toRomanUpper(tc.num)
|
||||
if result != tc.expected {
|
||||
t.Errorf("toRomanUpper(%d) = %s, 期望 %s", tc.num, result, tc.expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试边界情况
|
||||
if toRomanUpper(0) != "0" {
|
||||
t.Error("0应返回字符串'0'")
|
||||
}
|
||||
|
||||
if toRomanUpper(4000) != "4000" {
|
||||
t.Error("4000应返回字符串'4000'")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddCellListAllTypes 测试所有列表类型
|
||||
func TestAddCellListAllTypes(t *testing.T) {
|
||||
doc := New()
|
||||
|
||||
config := &TableConfig{
|
||||
Rows: 7,
|
||||
Cols: 1,
|
||||
Width: 3000,
|
||||
}
|
||||
|
||||
table, err := doc.CreateTable(config)
|
||||
if err != nil {
|
||||
t.Fatalf("创建表格失败: %v", err)
|
||||
}
|
||||
|
||||
testTypes := []struct {
|
||||
listType ListType
|
||||
name string
|
||||
}{
|
||||
{ListTypeBullet, "无序列表"},
|
||||
{ListTypeNumber, "数字列表"},
|
||||
{ListTypeDecimal, "十进制列表"},
|
||||
{ListTypeLowerLetter, "小写字母列表"},
|
||||
{ListTypeUpperLetter, "大写字母列表"},
|
||||
{ListTypeLowerRoman, "小写罗马列表"},
|
||||
{ListTypeUpperRoman, "大写罗马列表"},
|
||||
}
|
||||
|
||||
for i, tc := range testTypes {
|
||||
listConfig := &CellListConfig{
|
||||
Type: tc.listType,
|
||||
Items: []string{"项目1", "项目2", "项目3"},
|
||||
}
|
||||
|
||||
err := table.AddCellList(i, 0, listConfig)
|
||||
if err != nil {
|
||||
t.Errorf("添加%s失败: %v", tc.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user