-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2fa3052
commit 796b5cb
Showing
7 changed files
with
401 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,3 +19,6 @@ | |
|
||
# Go workspace file | ||
go.work | ||
|
||
|
||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package Record2Excel | ||
|
||
import ( | ||
"github.com/xuri/excelize/v2" | ||
"reflect" | ||
) | ||
|
||
type Exporter[T any] interface { | ||
AddRecord(record T) error | ||
Export() *excelize.File | ||
} | ||
|
||
type exporter[T any] struct { | ||
template template[T] | ||
file *excelize.File | ||
offset int | ||
index map[string]int | ||
} | ||
|
||
func NewExporter[T any](model T) (Exporter Exporter[T], err error) { | ||
var e = &exporter[T]{ | ||
file: excelize.NewFile(), | ||
index: make(map[string]int), | ||
} | ||
e.template, err = newTemplate(model) | ||
if err != nil { | ||
return nil, err | ||
} | ||
e.buildHeader() | ||
return e, nil | ||
} | ||
|
||
func (e *exporter[T]) buildHeader() { | ||
e.offset = e.template.items.depth() - 1 | ||
|
||
// 初始化Excel文件,如果还没有初始化 | ||
if e.file == nil { | ||
e.file = excelize.NewFile() | ||
} | ||
|
||
currentColumn := 1 | ||
var mergeRanges [][2]string // 用于记录需要合并的单元格范围 | ||
|
||
// 定义一个递归函数,用于构建表头并记录合并单元格的范围 | ||
var buildHeaderForRow func(node *itemNode, row int, parentColStart *int) | ||
buildHeaderForRow = func(node *itemNode, row int, parentColStart *int) { | ||
colStart := currentColumn // 当前节点开始的列 | ||
if parentColStart != nil { | ||
colStart = *parentColStart // 如果有父节点,从父节点的列开始 | ||
} | ||
|
||
// 设置单元格的值 | ||
cell, _ := excelize.CoordinatesToCellName(currentColumn, row) | ||
e.file.SetCellValue("Sheet1", cell, node.tagName) | ||
e.index[node.fieldPath] = currentColumn | ||
|
||
if len(node.subItems) == 0 { // 如果是叶子节点 | ||
if e.offset > row { // 需要跨行合并 | ||
cellEnd, _ := excelize.CoordinatesToCellName(currentColumn, e.offset) | ||
mergeRanges = append(mergeRanges, [2]string{cell, cellEnd}) | ||
} | ||
currentColumn++ // 移动到下一个列 | ||
} else { // 如果有子节点 | ||
for _, child := range node.subItems { | ||
buildHeaderForRow(child, row+1, &colStart) // 递归构建子节点表头 | ||
} | ||
if row == 1 { // 如果是第一层嵌套,记录合并的单元格范围 | ||
cellEnd, _ := excelize.CoordinatesToCellName(currentColumn-1, row) | ||
if cell != cellEnd { // 避免单列合并 | ||
mergeRanges = append(mergeRanges, [2]string{cell, cellEnd}) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// 从根节点开始递归 | ||
for _, item := range e.template.items.subItems { | ||
buildHeaderForRow(item, 1, nil) | ||
} | ||
|
||
// 执行合并单元格操作 | ||
for _, mergeRange := range mergeRanges { | ||
e.file.MergeCell("Sheet1", mergeRange[0], mergeRange[1]) | ||
} | ||
|
||
return | ||
} | ||
|
||
func (e *exporter[T]) AddRecord(record T) error { | ||
v := reflect.ValueOf(record) | ||
startIdx := e.offset + 1 | ||
|
||
var insert func(v reflect.Value, node *itemNode) | ||
insert = func(v reflect.Value, node *itemNode) { | ||
for _, item := range node.subItems { | ||
val := v.FieldByName(item.name) | ||
switch val.Kind() { | ||
case reflect.Struct: | ||
insert(val, item) | ||
|
||
case reflect.Slice: | ||
currentIdx := e.offset + 1 | ||
for i := 0; i < val.Len(); i++ { | ||
val := val.Index(i) | ||
if val.Kind() == reflect.Struct { | ||
tmpIdx := startIdx | ||
startIdx = currentIdx | ||
insert(val, item) | ||
startIdx = tmpIdx | ||
} else { | ||
cell, _ := excelize.CoordinatesToCellName(e.index[item.fieldPath], currentIdx) | ||
e.file.SetCellValue("Sheet1", cell, val.Interface()) | ||
} | ||
currentIdx++ | ||
} | ||
startIdx = max(startIdx, currentIdx) | ||
|
||
default: | ||
cell, _ := excelize.CoordinatesToCellName(e.index[item.fieldPath], startIdx) | ||
e.file.SetCellValue("Sheet1", cell, val.Interface()) | ||
} | ||
} | ||
return | ||
} | ||
|
||
insert(v, e.template.items) | ||
e.offset = startIdx - 1 | ||
return nil | ||
} | ||
|
||
func (e *exporter[T]) Export() *excelize.File { | ||
return e.file | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package Record2Excel | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func Test_exporter_buildHeader(t *testing.T) { | ||
type Achievements struct { | ||
Name string `excel:"名称"` | ||
Score float64 `excel:"分数"` | ||
ArchiveScore float64 `excel:"归档分数"` | ||
Achieved bool `excel:"是否达成"` | ||
} | ||
|
||
type Pins struct { | ||
Name string `excel:"名称"` | ||
Score float64 `excel:"分数"` | ||
} | ||
|
||
type Data struct { | ||
ID string `excel:"编号"` | ||
ProjectID string `excel:"项目编号"` | ||
StaffId string `excel:"员工编号"` | ||
StaffName string `excel:"员工姓名"` | ||
ClassNo string `excel:"班级编号"` | ||
Score float64 `excel:"总分"` | ||
Tags []string `excel:"标签"` | ||
Achievements []Achievements `excel:"成就"` | ||
Pins []Pins `excel:"奖项"` | ||
} | ||
s := Data{ | ||
ID: "123", | ||
ProjectID: "123", | ||
StaffId: "21", | ||
StaffName: "231", | ||
ClassNo: "21", | ||
Score: 23, | ||
Tags: []string{"@31", "321"}, | ||
Achievements: []Achievements{ | ||
{ | ||
Name: "123", | ||
Score: 23, | ||
ArchiveScore: 23, | ||
Achieved: true, | ||
}, | ||
{ | ||
Name: "123456", | ||
Score: 23, | ||
ArchiveScore: 23, | ||
Achieved: true, | ||
}, | ||
{ | ||
ArchiveScore: 23, | ||
Achieved: true, | ||
}, | ||
}, | ||
Pins: []Pins{ | ||
{ | ||
Name: "123", | ||
Score: 23, | ||
}, | ||
}, | ||
} | ||
|
||
e, _ := NewExporter(s) | ||
e.AddRecord(s) | ||
e.AddRecord(s) | ||
e.AddRecord(s) | ||
file := e.Export() | ||
file.SaveAs("test.xlsx") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module github.com/NX-Official/Record2Excel | ||
|
||
go 1.22.0 | ||
|
||
require github.com/xuri/excelize/v2 v2.8.1 | ||
|
||
require ( | ||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect | ||
github.com/richardlehane/mscfb v1.0.4 // indirect | ||
github.com/richardlehane/msoleps v1.0.3 // indirect | ||
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect | ||
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect | ||
golang.org/x/crypto v0.19.0 // indirect | ||
golang.org/x/net v0.21.0 // indirect | ||
golang.org/x/text v0.14.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= | ||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= | ||
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.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= | ||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= | ||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= | ||
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= | ||
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= | ||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= | ||
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= | ||
github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ= | ||
github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE= | ||
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= | ||
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= | ||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= | ||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= | ||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= | ||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= | ||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= | ||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | ||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package Record2Excel | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
) | ||
|
||
type template[T any] struct { | ||
t reflect.Type | ||
items *itemNode | ||
} | ||
|
||
type itemNode struct { | ||
name string | ||
tagName string | ||
fieldPath string | ||
subItems []*itemNode | ||
} | ||
|
||
func (t template[T]) depth() int { | ||
return t.depth() | ||
} | ||
func (n itemNode) depth() int { | ||
if len(n.subItems) == 0 { | ||
return 1 | ||
} | ||
maxDepth := 0 | ||
for _, child := range n.subItems { | ||
childDepth := child.depth() | ||
if childDepth > maxDepth { | ||
maxDepth = childDepth | ||
} | ||
} | ||
return maxDepth + 1 | ||
} | ||
|
||
func newTemplate[T any](model T) (template[T], error) { | ||
return template[T]{ | ||
t: reflect.TypeOf(model), | ||
items: buildItemTree(reflect.TypeOf(model), ""), | ||
}, nil | ||
} | ||
|
||
func buildItemTree(t reflect.Type, parentPath string) *itemNode { | ||
// 处理指针类型,我们需要其指向的元素类型 | ||
if t.Kind() == reflect.Ptr { | ||
t = t.Elem() | ||
} | ||
|
||
// 创建当前节点 | ||
node := &itemNode{ | ||
name: t.Name(), | ||
fieldPath: parentPath, | ||
subItems: []*itemNode{}, | ||
} | ||
|
||
if t.Kind() == reflect.Slice { | ||
t = t.Elem() | ||
} | ||
|
||
// 只有结构体类型才有子字段需要遍历 | ||
if t.Kind() == reflect.Struct { | ||
for i := 0; i < t.NumField(); i++ { | ||
field := t.Field(i) | ||
// 构建完整的字段路径 | ||
fieldPath := field.Name | ||
if parentPath != "" { | ||
fieldPath = parentPath + "." + field.Name | ||
} | ||
|
||
// 为当前字段创建 itemNode,并递归处理嵌套结构体字段 | ||
childNode := buildItemTree(field.Type, fieldPath) | ||
childNode.name = field.Name // 更新为实际的字段名 | ||
childNode.tagName = field.Name | ||
if name, ok := field.Tag.Lookup("excel"); ok { | ||
childNode.tagName = name | ||
} | ||
childNode.fieldPath = fieldPath | ||
node.subItems = append(node.subItems, childNode) | ||
} | ||
} | ||
return node | ||
} | ||
|
||
func (t template[T]) GetField(path string) (string, error) { | ||
val, err := getValueByPath(reflect.New(t.t).Elem().Interface(), path) | ||
if err != nil { | ||
return "", err | ||
} | ||
return fmt.Sprintf("%v", val), nil | ||
} | ||
|
||
func getValueByPath(v any, path string) (any, error) { | ||
// 将路径分割成部分 | ||
pathParts := strings.Split(path, ".") | ||
val := reflect.ValueOf(v) | ||
|
||
// 遍历路径的每一部分,逐步深入 | ||
for _, part := range pathParts { | ||
// 确保当前值可以被遍历 | ||
if val.Kind() == reflect.Ptr { | ||
val = val.Elem() | ||
} | ||
|
||
// 确保我们处理的是结构体 | ||
if val.Kind() != reflect.Struct { | ||
return nil, fmt.Errorf("not a struct or has no field '%s'", part) | ||
} | ||
|
||
// 获取指定的字段 | ||
val = val.FieldByName(part) | ||
if !val.IsValid() { | ||
return nil, fmt.Errorf("field not found: %s", part) | ||
} | ||
} | ||
|
||
// 返回找到的值 | ||
return val.Interface(), nil | ||
} |
Oops, something went wrong.