Skip to content

Commit

Permalink
feat: Support Basic Use
Browse files Browse the repository at this point in the history
  • Loading branch information
NX-Official committed Mar 26, 2024
1 parent 2fa3052 commit 796b5cb
Show file tree
Hide file tree
Showing 7 changed files with 401 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

# Go workspace file
go.work


.idea
133 changes: 133 additions & 0 deletions export.go
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
}
71 changes: 71 additions & 0 deletions export_test.go
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")
}
16 changes: 16 additions & 0 deletions go.mod
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
)
29 changes: 29 additions & 0 deletions go.sum
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=
120 changes: 120 additions & 0 deletions template.go
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
}
Loading

0 comments on commit 796b5cb

Please sign in to comment.