525 lines
13 KiB
Go
525 lines
13 KiB
Go
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)
|