359 lines
8.2 KiB
Go
359 lines
8.2 KiB
Go
|
|
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)
|
|||
|
|
}
|