Initialize office module (re-engineered from gojs/office by AI)
This commit is contained in:
commit
810cdd1fda
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.geminiignore
|
||||
.gemini
|
||||
.ai/
|
||||
env.json
|
||||
env.yml
|
||||
env.yaml
|
||||
.log.meta.json
|
||||
9
CHANGELOG.md
Normal file
9
CHANGELOG.md
Normal file
@ -0,0 +1,9 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.0.0 (2026-05-12)
|
||||
|
||||
- 从 `gojs/office` 迁移并重构为纯 Go 实现。
|
||||
- 基础设施对齐:使用 `apigo.cc/go/cast` 和 `apigo.cc/go/file`。
|
||||
- 移除 `gojs` 和 `goja` 相关依赖。
|
||||
- 优化 `SetData` 和 `GetData` 的逻辑,支持自动扩展列。
|
||||
- 提供更简洁的 `Open` 和 `New` 入口。
|
||||
72
README.md
Normal file
72
README.md
Normal file
@ -0,0 +1,72 @@
|
||||
# office
|
||||
|
||||
极简、高效的 Go Excel 处理库,基于 `excelize` 构建,符合 `@go` 设计哲学。
|
||||
|
||||
## 特性
|
||||
|
||||
- **统一 API**: 提供极简的 `Open`, `Save`, `Get`, `Set` 等操作。
|
||||
- **自动对齐**: 自动处理工作表的创建和索引。
|
||||
- **对象映射**: 支持将 `[]map[string]any` 直接写入 Excel 或从 Excel 读取。
|
||||
- **高性能**: 尽量减少内存分配和冗余操作。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
go get apigo.cc/go/office
|
||||
```
|
||||
|
||||
### 基础用法
|
||||
|
||||
```go
|
||||
import "apigo.cc/go/office"
|
||||
|
||||
// 创建并写入
|
||||
xls := office.New()
|
||||
table := [][]any{
|
||||
{"Name", "Age"},
|
||||
{"Alice", 25},
|
||||
{"Bob", 30},
|
||||
}
|
||||
xls.Set("Sheet1", table, "A1", "")
|
||||
xls.Save("example.xlsx")
|
||||
|
||||
// 读取
|
||||
xls2, _ := office.Open("example.xlsx")
|
||||
data, _ := xls2.Get("Sheet1", "A1", "")
|
||||
```
|
||||
|
||||
### 对象列表操作
|
||||
|
||||
```go
|
||||
data := []map[string]any{
|
||||
{"Name": "Alice", "Age": 25},
|
||||
{"Name": "Bob", "Age": 30},
|
||||
}
|
||||
xls.SetData("Users", data, "A1", "")
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### 核心函数
|
||||
|
||||
- `New() *Excel`: 创建新的 Excel 对象。
|
||||
- `Open(filename string, password ...string) (*Excel, error)`: 打开现有文件。
|
||||
|
||||
### Excel 方法
|
||||
|
||||
- `Save(filename ...string) error`: 保存文件。
|
||||
- `Bytes() ([]byte, error)`: 获取字节切片。
|
||||
- `Set(sheetName string, table [][]any, start, end string) error`: 写入二维数据。
|
||||
- `Get(sheetName string, start, end string) ([][]any, error)`: 读取二维数据。
|
||||
- `SetData(sheetName string, data []map[string]any, start, end string) error`: 写入对象列表。
|
||||
- `GetData(sheetName string, start, end string) ([]map[string]any, error)`: 读取对象列表。
|
||||
- `Sheets() []string`: 获取工作表列表。
|
||||
- `RemoveSheet(sheetName string) error`: 删除工作表。
|
||||
|
||||
### 工具函数
|
||||
|
||||
- `MakeCellID(col, row int) string`: 生成单元格 ID (如 "A1")。
|
||||
- `ParseCellID(cell string) (col, row int)`: 解析单元格 ID。
|
||||
- `MakeColID(col int) string`: 生成列 ID (如 "A")。
|
||||
358
excel.go
Normal file
358
excel.go
Normal file
@ -0,0 +1,358 @@
|
||||
package office
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"apigo.cc/go/cast"
|
||||
"apigo.cc/go/file"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// Excel 封装了 Excel 文件的核心操作,提供极简且高效的 API。
|
||||
type Excel struct {
|
||||
filename string
|
||||
password string
|
||||
excel *excelize.File
|
||||
}
|
||||
|
||||
// New 创建一个新的 Excel 对象。
|
||||
func New() *Excel {
|
||||
return &Excel{
|
||||
excel: excelize.NewFile(),
|
||||
}
|
||||
}
|
||||
|
||||
// Open 打开一个现有的 Excel 文件。
|
||||
// 如果文件不存在,将创建一个新的 Excel 对象。
|
||||
func Open(filename string, password ...string) (*Excel, error) {
|
||||
pwd := ""
|
||||
if len(password) > 0 {
|
||||
pwd = password[0]
|
||||
}
|
||||
|
||||
xls := &Excel{
|
||||
filename: filename,
|
||||
password: pwd,
|
||||
}
|
||||
|
||||
if file.Exists(filename) {
|
||||
f, err := excelize.OpenFile(filename, excelize.Options{Password: pwd})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
xls.excel = f
|
||||
} else {
|
||||
xls.excel = excelize.NewFile()
|
||||
}
|
||||
|
||||
return xls, nil
|
||||
}
|
||||
|
||||
// Save 保存 Excel 文件。
|
||||
// 如果提供了 filename,将另存为该文件名。
|
||||
func (xls *Excel) Save(filename ...string) error {
|
||||
if len(filename) > 0 && filename[0] != "" {
|
||||
xls.filename = filename[0]
|
||||
}
|
||||
if xls.filename == "" {
|
||||
return fmt.Errorf("no filename specified")
|
||||
}
|
||||
return xls.excel.SaveAs(xls.filename, excelize.Options{Password: xls.password})
|
||||
}
|
||||
|
||||
// Bytes 将 Excel 内容写入字节切片。
|
||||
func (xls *Excel) Bytes() ([]byte, error) {
|
||||
buf, err := xls.excel.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Set 设置指定单元格范围的值。
|
||||
// table: 二维数组,代表行和列。
|
||||
// start: 起始单元格 ID(如 "A1"),默认为 "A1"。
|
||||
// end: 结束单元格 ID(如 "C3"),用于限制写入范围。
|
||||
func (xls *Excel) Set(sheetName string, table [][]any, start, end string) error {
|
||||
sheet := xls.getOrCreateSheet(sheetName)
|
||||
startX, startY := ParseCellID(start)
|
||||
endX, endY := ParseCellID(end)
|
||||
|
||||
for y, row := range table {
|
||||
if endY > 0 && startY+y > endY {
|
||||
break
|
||||
}
|
||||
for x, v := range row {
|
||||
if endX > 0 && startX+x > endX {
|
||||
break
|
||||
}
|
||||
cellID := MakeCellID(startX+x, startY+y)
|
||||
if err := xls.excel.SetCellValue(sheet, cellID, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取指定范围的单元格数据。
|
||||
func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) {
|
||||
sheet := xls.getOrCreateSheet(sheetName)
|
||||
rows, err := xls.excel.GetRows(sheet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startX, startY := ParseCellID(start)
|
||||
endX, endY := ParseCellID(end)
|
||||
|
||||
result := make([][]any, 0)
|
||||
for y, row := range rows {
|
||||
if startY > 0 && y < startY {
|
||||
continue
|
||||
}
|
||||
if endY > 0 && y > endY {
|
||||
break
|
||||
}
|
||||
|
||||
rowData := make([]any, 0)
|
||||
for x, v := range row {
|
||||
if startX > 0 && x < startX {
|
||||
continue
|
||||
}
|
||||
if endX > 0 && x > endX {
|
||||
break
|
||||
}
|
||||
|
||||
// 尝试根据单元格类型转换数据
|
||||
cellID := MakeCellID(x, y)
|
||||
cellType, _ := xls.excel.GetCellType(sheet, cellID)
|
||||
|
||||
switch cellType {
|
||||
case excelize.CellTypeNumber:
|
||||
if isFloat(v) {
|
||||
rowData = append(rowData, cast.To[float64](v))
|
||||
} else {
|
||||
rowData = append(rowData, cast.To[int64](v))
|
||||
}
|
||||
case excelize.CellTypeBool:
|
||||
rowData = append(rowData, cast.To[bool](v))
|
||||
case excelize.CellTypeDate:
|
||||
// TODO: 更好的日期处理
|
||||
rowData = append(rowData, v)
|
||||
default:
|
||||
rowData = append(rowData, v)
|
||||
}
|
||||
}
|
||||
result = append(result, rowData)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RemoveSheet 删除工作表。
|
||||
func (xls *Excel) RemoveSheet(sheetName string) error {
|
||||
return xls.excel.DeleteSheet(sheetName)
|
||||
}
|
||||
|
||||
// Sheets 返回所有工作表名称。
|
||||
func (xls *Excel) Sheets() []string {
|
||||
return xls.excel.GetSheetList()
|
||||
}
|
||||
|
||||
// SetColWidth 设置列宽。
|
||||
func (xls *Excel) SetColWidth(sheetName string, startCol, endCol string, width float64) error {
|
||||
return xls.excel.SetColWidth(xls.getOrCreateSheet(sheetName), startCol, endCol, width)
|
||||
}
|
||||
|
||||
// SetColWidths 批量设置列宽。
|
||||
func (xls *Excel) SetColWidths(sheetName string, widths []float64) error {
|
||||
sheet := xls.getOrCreateSheet(sheetName)
|
||||
for i, w := range widths {
|
||||
col := MakeColID(i)
|
||||
if err := xls.excel.SetColWidth(sheet, col, col, w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetCellStyle 设置单元格样式。
|
||||
func (xls *Excel) SetCellStyle(sheetName string, start, end string, styleID int) error {
|
||||
return xls.excel.SetCellStyle(xls.getOrCreateSheet(sheetName), start, end, styleID)
|
||||
}
|
||||
|
||||
// MakeStyle 创建样式并返回样式 ID。
|
||||
func (xls *Excel) MakeStyle(style *excelize.Style) (int, error) {
|
||||
return xls.excel.NewStyle(style)
|
||||
}
|
||||
|
||||
// SetPanes 设置冻结窗格。
|
||||
func (xls *Excel) SetPanes(sheetName string, panes *excelize.Panes) error {
|
||||
return xls.excel.SetPanes(xls.getOrCreateSheet(sheetName), panes)
|
||||
}
|
||||
|
||||
// SetAutoFilter 设置自动筛选。
|
||||
func (xls *Excel) SetAutoFilter(sheetName string, start, end string, options []excelize.AutoFilterOptions) error {
|
||||
return xls.excel.AutoFilter(xls.getOrCreateSheet(sheetName), start+":"+end, options)
|
||||
}
|
||||
|
||||
// SetData 将对象列表写入 Excel。
|
||||
func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end string) error {
|
||||
table, err := xls.Get(sheetName, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fieldIndex := map[string]int{}
|
||||
fieldNum := 0
|
||||
if len(table) > 0 {
|
||||
fieldNum = len(table[0])
|
||||
for i, v := range table[0] {
|
||||
fieldIndex[cast.To[string](v)] = i
|
||||
}
|
||||
} else {
|
||||
table = append(table, []any{})
|
||||
}
|
||||
|
||||
for i, item := range data {
|
||||
for len(table) <= i+1 {
|
||||
table = append(table, make([]any, fieldNum))
|
||||
}
|
||||
for k, v := range item {
|
||||
idx, ok := fieldIndex[k]
|
||||
if !ok {
|
||||
idx = fieldNum
|
||||
fieldIndex[k] = idx
|
||||
fieldNum++
|
||||
// 扩展所有行以匹配新的列数
|
||||
for r := range table {
|
||||
for len(table[r]) < fieldNum {
|
||||
table[r] = append(table[r], "")
|
||||
}
|
||||
}
|
||||
table[0][idx] = k
|
||||
}
|
||||
table[i+1][idx] = v
|
||||
}
|
||||
}
|
||||
return xls.Set(sheetName, table, start, end)
|
||||
}
|
||||
|
||||
// GetData 从 Excel 中读取对象列表。
|
||||
func (xls *Excel) GetData(sheetName string, start, end string) ([]map[string]any, error) {
|
||||
rows, err := xls.Get(sheetName, start, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fields := make([]string, 0)
|
||||
for i, v := range rows[0] {
|
||||
name := cast.To[string](v)
|
||||
if name == "" {
|
||||
name = MakeColID(i)
|
||||
}
|
||||
fields = append(fields, name)
|
||||
}
|
||||
|
||||
data := make([]map[string]any, 0)
|
||||
for i := 1; i < len(rows); i++ {
|
||||
rowMap := map[string]any{}
|
||||
for j, field := range fields {
|
||||
if j < len(rows[i]) {
|
||||
rowMap[field] = rows[i][j]
|
||||
} else {
|
||||
rowMap[field] = nil
|
||||
}
|
||||
}
|
||||
data = append(data, rowMap)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 辅助方法:获取或创建工作表
|
||||
func (xls *Excel) getOrCreateSheet(name string) string {
|
||||
if name == "" {
|
||||
name = "Sheet1"
|
||||
}
|
||||
// 如果是数字索引
|
||||
if matched, _ := regexp.MatchString(`^\d+$`, name); matched {
|
||||
idx := cast.To[int](name)
|
||||
return xls.excel.GetSheetName(idx)
|
||||
}
|
||||
|
||||
idx, _ := xls.excel.GetSheetIndex(name)
|
||||
if idx == -1 {
|
||||
xls.excel.NewSheet(name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// MakeCellID 根据行列索引生成单元格 ID(如 0, 0 -> "A1")。
|
||||
func MakeCellID(col, row int) string {
|
||||
return MakeColID(col) + strconv.Itoa(row+1)
|
||||
}
|
||||
|
||||
// MakeColID 根据列索引生成列 ID(如 0 -> "A", 26 -> "AA")。
|
||||
func MakeColID(col int) string {
|
||||
colName := ""
|
||||
for col >= 0 {
|
||||
colName = string(rune(col%26+65)) + colName
|
||||
col = col/26 - 1
|
||||
}
|
||||
return colName
|
||||
}
|
||||
|
||||
// ParseCellID 解析单元格 ID(如 "A1" -> 0, 0)。
|
||||
func ParseCellID(cell string) (col, row int) {
|
||||
if cell == "" {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// 找到第一个数字的位置
|
||||
numIdx := -1
|
||||
for i, r := range cell {
|
||||
if r >= '0' && r <= '9' {
|
||||
numIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if numIdx == -1 {
|
||||
// 只有字母,当做列处理
|
||||
return parseCol(cell), 0
|
||||
}
|
||||
|
||||
col = parseCol(cell[:numIdx])
|
||||
row, _ = strconv.Atoi(cell[numIdx:])
|
||||
row-- // 转为 0 索引
|
||||
if row < 0 {
|
||||
row = 0
|
||||
}
|
||||
return col, row
|
||||
}
|
||||
|
||||
func parseCol(colStr string) int {
|
||||
col := 0
|
||||
for _, r := range colStr {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
col = col*26 + int(r-'A'+1)
|
||||
} else if r >= 'a' && r <= 'z' {
|
||||
col = col*26 + int(r-'a'+1)
|
||||
}
|
||||
}
|
||||
return col - 1
|
||||
}
|
||||
|
||||
var floatMatcher = regexp.MustCompile(`^[\d.]{1,18}$`)
|
||||
|
||||
func isFloat(v any) bool {
|
||||
s := cast.To[string](v)
|
||||
return bytes.ContainsRune([]byte(s), '.') && floatMatcher.MatchString(s)
|
||||
}
|
||||
119
excel_test.go
Normal file
119
excel_test.go
Normal file
@ -0,0 +1,119 @@
|
||||
package office
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/file"
|
||||
)
|
||||
|
||||
func TestExcel_Basic(t *testing.T) {
|
||||
filename := "test_basic.xlsx"
|
||||
defer file.Remove(filename)
|
||||
|
||||
xls := New()
|
||||
table := [][]any{
|
||||
{"Name", "Age", "City"},
|
||||
{"Alice", 25, "New York"},
|
||||
{"Bob", 30, "London"},
|
||||
}
|
||||
|
||||
err := xls.Set("Sheet1", table, "A1", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Set failed: %v", err)
|
||||
}
|
||||
|
||||
err = xls.Save(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Save failed: %v", err)
|
||||
}
|
||||
|
||||
xls2, err := Open(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
|
||||
data, err := xls2.Get("Sheet1", "A1", "C3")
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
|
||||
if len(data) != 3 || len(data[1]) != 3 {
|
||||
t.Errorf("Unexpected data length: %v", data)
|
||||
}
|
||||
|
||||
if castTo[string](data[1][0]) != "Alice" {
|
||||
t.Errorf("Expected Alice, got %v", data[1][0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExcel_Data(t *testing.T) {
|
||||
filename := "test_data.xlsx"
|
||||
defer file.Remove(filename)
|
||||
|
||||
xls := New()
|
||||
data := []map[string]any{
|
||||
{"ID": 1, "Value": "A"},
|
||||
{"ID": 2, "Value": "B"},
|
||||
}
|
||||
|
||||
err := xls.SetData("Data", data, "A1", "")
|
||||
if err != nil {
|
||||
t.Fatalf("SetData failed: %v", err)
|
||||
}
|
||||
|
||||
err = xls.Save(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Save failed: %v", err)
|
||||
}
|
||||
|
||||
xls2, err := Open(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
|
||||
readData, err := xls2.GetData("Data", "A1", "")
|
||||
if err != nil {
|
||||
t.Fatalf("GetData failed: %v", err)
|
||||
}
|
||||
|
||||
if len(readData) != 2 {
|
||||
t.Errorf("Expected 2 rows, got %d", len(readData))
|
||||
}
|
||||
|
||||
if castTo[string](readData[0]["Value"]) != "A" {
|
||||
t.Errorf("Expected A, got %v", readData[0]["Value"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIDGeneration(t *testing.T) {
|
||||
tests := []struct {
|
||||
col, row int
|
||||
expected string
|
||||
}{
|
||||
{0, 0, "A1"},
|
||||
{25, 0, "Z1"},
|
||||
{26, 0, "AA1"},
|
||||
{0, 9, "A10"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := MakeCellID(tt.col, tt.row)
|
||||
if got != tt.expected {
|
||||
t.Errorf("MakeCellID(%d, %d) = %s; want %s", tt.col, tt.row, got, tt.expected)
|
||||
}
|
||||
|
||||
c, r := ParseCellID(tt.expected)
|
||||
if c != tt.col || r != tt.row {
|
||||
t.Errorf("ParseCellID(%s) = (%d, %d); want (%d, %d)", tt.expected, c, r, tt.col, tt.row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for test assertions
|
||||
func castTo[T any](v any) T {
|
||||
var res T
|
||||
if val, ok := v.(T); ok {
|
||||
return val
|
||||
}
|
||||
return res
|
||||
}
|
||||
26
go.mod
Normal file
26
go.mod
Normal file
@ -0,0 +1,26 @@
|
||||
module apigo.cc/go/office
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
apigo.cc/go/cast v1.3.0
|
||||
apigo.cc/go/file v1.3.0
|
||||
github.com/xuri/excelize/v2 v2.10.1
|
||||
)
|
||||
|
||||
require (
|
||||
apigo.cc/go/encoding v1.3.0 // indirect
|
||||
apigo.cc/go/rand v1.3.0 // indirect
|
||||
apigo.cc/go/safe v1.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
50
go.sum
Normal file
50
go.sum
Normal file
@ -0,0 +1,50 @@
|
||||
apigo.cc/go/cast v1.3.0 h1:ZTcLYijkqZjSWSCSpJUWMfzJYeJKbwKxquKkPrFsROQ=
|
||||
apigo.cc/go/cast v1.3.0/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
|
||||
apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo=
|
||||
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
|
||||
apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY=
|
||||
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
|
||||
apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
|
||||
apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
|
||||
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Loading…
x
Reference in New Issue
Block a user