Files
go-common/excel/excel.go

525 lines
13 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package excel
import (
"fmt"
"io"
"reflect"
"time"
"git.toowon.com/jimmy/go-common/tools"
"github.com/xuri/excelize/v2"
)
// Excel Excel导出器
type Excel struct {
file *excelize.File
}
// NewExcel 创建Excel导出器
func NewExcel() *Excel {
return &Excel{
file: excelize.NewFile(),
}
}
// ExportColumn 导出列定义
type ExportColumn struct {
// Header 表头名称
Header string
// Field 数据字段名(支持嵌套字段,如 "User.Name"
Field string
// Width 列宽可选0表示自动
Width float64
// Format 格式化函数(可选,用于自定义字段值的格式化)
Format func(value interface{}) string
}
// ExportData 导出数据接口
// 实现此接口的结构体可以直接导出
type ExportData interface {
// GetExportColumns 获取导出列定义
GetExportColumns() []ExportColumn
// GetExportRows 获取导出数据行
GetExportRows() [][]interface{}
}
// ExportToWriter 导出数据到Writer黑盒模式推荐使用
// sheetName: 工作表名称(可选,默认为"Sheet1"
// columns: 列定义
// data: 数据列表可以是结构体切片或实现了ExportData接口的对象
// 返回错误信息
//
// 示例1导出结构体切片
//
// type User struct {
// ID int `json:"id"`
// Name string `json:"name"`
// Email string `json:"email"`
// }
//
// users := []User{
// {ID: 1, Name: "Alice", Email: "alice@example.com"},
// {ID: 2, Name: "Bob", Email: "bob@example.com"},
// }
//
// columns := []ExportColumn{
// {Header: "ID", Field: "ID"},
// {Header: "姓名", Field: "Name"},
// {Header: "邮箱", Field: "Email"},
// }
//
// excel := excel.NewExcel()
// err := excel.ExportToWriter(w, "用户列表", columns, users)
//
// 示例2使用格式化函数
//
// columns := []ExportColumn{
// {Header: "ID", Field: "ID"},
// {Header: "姓名", Field: "Name"},
// {Header: "创建时间", Field: "CreatedAt", Format: func(v interface{}) string {
// if t, ok := v.(time.Time); ok {
// return t.Format("2006-01-02 15:04:05")
// }
// return ""
// }},
// }
//
// excel.ExportToWriter(w, "用户列表", columns, users)
func (e *Excel) ExportToWriter(w io.Writer, sheetName string, columns []ExportColumn, data interface{}) error {
if e.file == nil {
e.file = excelize.NewFile()
}
// 设置工作表名称
if sheetName == "" {
sheetName = "Sheet1"
}
// 检查工作表是否已存在
sheetIndex, err := e.file.GetSheetIndex(sheetName)
if err != nil || sheetIndex == 0 {
// 工作表不存在,需要创建
// 如果sheetName不是"Sheet1",且默认"Sheet1"存在,则删除它
if sheetName != "Sheet1" {
defaultIndex, _ := e.file.GetSheetIndex("Sheet1")
if defaultIndex > 0 {
e.file.DeleteSheet("Sheet1")
}
}
// 创建新工作表
_, err = e.file.NewSheet(sheetName)
if err != nil {
return fmt.Errorf("failed to create sheet: %w", err)
}
// 重新获取工作表索引
sheetIndex, err = e.file.GetSheetIndex(sheetName)
if err != nil {
return fmt.Errorf("failed to get sheet index: %w", err)
}
}
// 设置活动工作表
if sheetIndex > 0 {
e.file.SetActiveSheet(sheetIndex)
}
// 写入表头
headerStyle, _ := e.file.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
Size: 12,
},
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#E0E0E0"},
Pattern: 1,
},
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
},
})
for i, col := range columns {
cell := fmt.Sprintf("%c1", 'A'+i)
e.file.SetCellValue(sheetName, cell, col.Header)
// 设置表头样式
e.file.SetCellStyle(sheetName, cell, cell, headerStyle)
// 设置列宽
if col.Width > 0 {
colName, _ := excelize.ColumnNumberToName(i + 1)
e.file.SetColWidth(sheetName, colName, colName, col.Width)
}
}
// 处理数据
var rows [][]interface{}
// 检查数据是否实现了ExportData接口
if exportData, ok := data.(ExportData); ok {
// 使用接口方法获取数据
rows = exportData.GetExportRows()
} else {
// 处理结构体切片
rows, err = e.convertDataToRows(data, columns)
if err != nil {
return fmt.Errorf("failed to convert data to rows: %w", err)
}
}
// 写入数据行
for rowIndex, row := range rows {
for colIndex, value := range row {
cell := fmt.Sprintf("%c%d", 'A'+colIndex, rowIndex+2) // +2 因为第一行是表头
// 应用格式化函数
var cellValue interface{} = value
if colIndex < len(columns) && columns[colIndex].Format != nil {
cellValue = columns[colIndex].Format(value)
}
e.file.SetCellValue(sheetName, cell, cellValue)
}
}
// 自动调整列宽(如果未设置宽度)
for i, col := range columns {
if col.Width == 0 {
colName, _ := excelize.ColumnNumberToName(i + 1)
// 获取列的最大宽度
maxWidth := e.getColumnMaxWidth(sheetName, i+1, len(rows)+1)
if maxWidth > 0 {
e.file.SetColWidth(sheetName, colName, colName, maxWidth)
}
}
}
// 写入到Writer
return e.file.Write(w)
}
// ExportToFile 导出数据到文件(黑盒模式,推荐使用)
// filePath: 文件路径
// sheetName: 工作表名称(可选,默认为"Sheet1"
// columns: 列定义
// data: 数据列表
// 返回错误信息
//
// 示例:
//
// excel := excel.NewExcel()
// err := excel.ExportToFile("users.xlsx", "用户列表", columns, users)
func (e *Excel) ExportToFile(filePath string, sheetName string, columns []ExportColumn, data interface{}) error {
if e.file == nil {
e.file = excelize.NewFile()
}
// 设置工作表名称
if sheetName == "" {
sheetName = "Sheet1"
}
// 检查工作表是否已存在
sheetIndex, err := e.file.GetSheetIndex(sheetName)
if err != nil || sheetIndex == 0 {
// 工作表不存在,需要创建
// 如果sheetName不是"Sheet1",且默认"Sheet1"存在,则删除它
if sheetName != "Sheet1" {
defaultIndex, _ := e.file.GetSheetIndex("Sheet1")
if defaultIndex > 0 {
e.file.DeleteSheet("Sheet1")
}
}
// 创建新工作表
_, err = e.file.NewSheet(sheetName)
if err != nil {
return fmt.Errorf("failed to create sheet: %w", err)
}
// 重新获取工作表索引
sheetIndex, err = e.file.GetSheetIndex(sheetName)
if err != nil {
return fmt.Errorf("failed to get sheet index: %w", err)
}
}
// 设置活动工作表
if sheetIndex > 0 {
e.file.SetActiveSheet(sheetIndex)
}
// 写入表头
headerStyle, _ := e.file.NewStyle(&excelize.Style{
Font: &excelize.Font{
Bold: true,
Size: 12,
},
Fill: excelize.Fill{
Type: "pattern",
Color: []string{"#E0E0E0"},
Pattern: 1,
},
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
},
})
for i, col := range columns {
cell := fmt.Sprintf("%c1", 'A'+i)
e.file.SetCellValue(sheetName, cell, col.Header)
// 设置表头样式
e.file.SetCellStyle(sheetName, cell, cell, headerStyle)
// 设置列宽
if col.Width > 0 {
colName, _ := excelize.ColumnNumberToName(i + 1)
e.file.SetColWidth(sheetName, colName, colName, col.Width)
}
}
// 处理数据
var rows [][]interface{}
// 检查数据是否实现了ExportData接口
if exportData, ok := data.(ExportData); ok {
// 使用接口方法获取数据
rows = exportData.GetExportRows()
} else {
// 处理结构体切片
rows, err = e.convertDataToRows(data, columns)
if err != nil {
return fmt.Errorf("failed to convert data to rows: %w", err)
}
}
// 写入数据行
for rowIndex, row := range rows {
for colIndex, value := range row {
cell := fmt.Sprintf("%c%d", 'A'+colIndex, rowIndex+2) // +2 因为第一行是表头
// 应用格式化函数
var cellValue interface{} = value
if colIndex < len(columns) && columns[colIndex].Format != nil {
cellValue = columns[colIndex].Format(value)
}
e.file.SetCellValue(sheetName, cell, cellValue)
}
}
// 自动调整列宽(如果未设置宽度)
for i, col := range columns {
if col.Width == 0 {
colName, _ := excelize.ColumnNumberToName(i + 1)
// 获取列的最大宽度
maxWidth := e.getColumnMaxWidth(sheetName, i+1, len(rows)+1)
if maxWidth > 0 {
e.file.SetColWidth(sheetName, colName, colName, maxWidth)
}
}
}
// 保存文件
return e.file.SaveAs(filePath)
}
// GetFile 获取Excel文件对象高级功能时使用
// 返回excelize.File对象可用于高级操作
//
// 推荐使用黑盒方法:
// - ExportToWriter()导出到Writer
// - ExportToFile():导出到文件
//
// 仅在需要使用高级功能时获取对象:
// - 多工作表操作
// - 自定义样式
// - 图表、公式等高级功能
//
// 示例(常用操作,推荐):
//
// excel := excel.NewExcel()
// excel.ExportToFile("users.xlsx", "用户列表", columns, users)
//
// 示例(高级功能):
//
// file := excel.GetFile()
// file.NewSheet("Sheet2")
// file.SetCellValue("Sheet2", "A1", "数据")
func (e *Excel) GetFile() *excelize.File {
if e.file == nil {
e.file = excelize.NewFile()
}
return e.file
}
// convertDataToRows 将数据转换为行数据
func (e *Excel) convertDataToRows(data interface{}, columns []ExportColumn) ([][]interface{}, error) {
// 使用反射处理数据
val := reflect.ValueOf(data)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Slice {
return nil, fmt.Errorf("data must be a slice")
}
rows := make([][]interface{}, 0, val.Len())
for i := 0; i < val.Len(); i++ {
item := val.Index(i)
if item.Kind() == reflect.Ptr {
item = item.Elem()
}
row := make([]interface{}, len(columns))
for j, col := range columns {
value := e.getFieldValue(item, col.Field)
row[j] = value
}
rows = append(rows, row)
}
return rows, nil
}
// getFieldValue 获取字段值(支持嵌套字段)
func (e *Excel) getFieldValue(v reflect.Value, fieldPath string) interface{} {
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil
}
// 处理嵌套字段(如 "User.Name"
fields := splitFieldPath(fieldPath)
current := v
for i, fieldName := range fields {
if current.Kind() == reflect.Ptr {
current = current.Elem()
}
if current.Kind() != reflect.Struct {
return nil
}
field := current.FieldByName(fieldName)
if !field.IsValid() {
return nil
}
// 如果是最后一个字段,返回值
if i == len(fields)-1 {
if field.CanInterface() {
return field.Interface()
}
return nil
}
// 继续嵌套查找
current = field
}
return nil
}
// splitFieldPath 分割字段路径(如 "User.Name" -> ["User", "Name"]
func splitFieldPath(path string) []string {
result := make([]string, 0)
current := ""
for _, char := range path {
if char == '.' {
if current != "" {
result = append(result, current)
current = ""
}
} else {
current += string(char)
}
}
if current != "" {
result = append(result, current)
}
return result
}
// getColumnMaxWidth 获取列的最大宽度(用于自动调整列宽)
func (e *Excel) getColumnMaxWidth(sheetName string, colIndex int, maxRow int) float64 {
maxWidth := 10.0 // 默认最小宽度
for row := 1; row <= maxRow; row++ {
cell, _ := excelize.CoordinatesToCellName(colIndex, row)
value, err := e.file.GetCellValue(sheetName, cell)
if err == nil {
width := float64(len(value)) + 2 // 加2作为边距
if width > maxWidth {
maxWidth = width
}
}
}
// 限制最大宽度
if maxWidth > 50 {
maxWidth = 50
}
return maxWidth
}
// AdaptTimeFormatter 适配器函数将tools包的格式化函数转换为Excel Format字段需要的函数类型
// 允许直接使用tools包的任何格式化函数
//
// 示例:
//
// // 直接使用tools.FormatDate
// Format: excel.AdaptTimeFormatter(tools.FormatDate)
//
// // 使用自定义格式化函数
// Format: excel.AdaptTimeFormatter(func(t time.Time) string {
// return tools.Format(t, "2006-01-02 15:04:05", "Asia/Shanghai")
// })
func AdaptTimeFormatter(fn func(time.Time, ...string) string) func(interface{}) string {
return func(value interface{}) string {
if t, ok := value.(time.Time); ok {
return fn(t)
}
return ""
}
}
// formatDateTime 格式化日期时间(内部便捷函数)
// 用于ExportColumn的Format字段
// layout: 时间格式,如 "2006-01-02 15:04:05"
// timezone: 可选时区,如果为空则使用时间对象本身的时区
// 直接调用 tools.Format() 方法
func formatDateTime(layout string, timezone ...string) func(interface{}) string {
return AdaptTimeFormatter(func(t time.Time, _ ...string) string {
return tools.Format(t, layout, timezone...)
})
}
// formatDate 格式化日期(内部便捷函数)
// 用于ExportColumn的Format字段格式2006-01-02
// 直接调用 tools.FormatDate() 方法
var formatDate = AdaptTimeFormatter(tools.FormatDate)
// formatDateTimeDefault 格式化日期时间(内部便捷函数)
// 用于ExportColumn的Format字段格式2006-01-02 15:04:05
// 直接调用 tools.FormatDateTime() 方法
var formatDateTimeDefault = AdaptTimeFormatter(tools.FormatDateTime)
// formatTime 格式化时间(内部便捷函数)
// 用于ExportColumn的Format字段格式15:04:05
// 直接调用 tools.FormatTime() 方法
var formatTime = AdaptTimeFormatter(tools.FormatTime)