diff --git a/export/cell.go b/export/cell.go new file mode 100644 index 0000000..41c2789 --- /dev/null +++ b/export/cell.go @@ -0,0 +1,36 @@ +package export + +import ( + "fmt" + + "github.com/xuri/excelize/v2" +) + +type Cell struct { + Value any + // 标题位置(列) + Location Location + // 标题合并列数(除开本身) + Colspan int + // 标题合并行数(除开本身) + Rowspan int + Style *excelize.Style +} + +func (e *Exporter) SetCell(sheet string, cell Cell) { + start_col, end_col := e.CaculateCell(cell.Location, cell.Rowspan, cell.Colspan) + e.xlsx.MergeCell(sheet, start_col, end_col) + e.xlsx.SetCellValue(sheet, start_col, cell.Value) + if cell.Style != nil { + style, _ := e.xlsx.NewStyle(cell.Style) + e.xlsx.SetCellStyle(sheet, start_col, end_col, style) + } +} + +func (e *Exporter) CaculateCell(location Location, rowSpan, colSpan int) (string, string) { + col_name, _ := excelize.ColumnNumberToName(location.X) + start_col := fmt.Sprintf("%s%d", col_name, location.Y) + h_pos, _ := excelize.ColumnNumberToName(location.X + rowSpan) + end_col := fmt.Sprintf("%s%d", h_pos, location.Y+colSpan) + return start_col, end_col +} diff --git a/export/style.go b/export/style.go new file mode 100644 index 0000000..8eeb7c8 --- /dev/null +++ b/export/style.go @@ -0,0 +1,42 @@ +package export + +import "github.com/xuri/excelize/v2" + +func DefaultTitleStyle() *excelize.Style { + return &excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + }, + Font: &excelize.Font{ + Bold: true, + }, + Fill: excelize.Fill{ + Type: "pattern", + Color: []string{"4aa4ea"}, + Pattern: 1, + }, + Border: []excelize.Border{ + { + Type: "left", + Color: "000000", + Style: 1, + }, + { + Type: "top", + Color: "000000", + Style: 1, + }, + { + Type: "right", + Color: "000000", + Style: 1, + }, + { + Type: "bottom", + Color: "000000", + Style: 1, + }, + }, + } +} diff --git a/export/title.go b/export/title.go new file mode 100644 index 0000000..dd7be12 --- /dev/null +++ b/export/title.go @@ -0,0 +1,40 @@ +package export + +import "github.com/xuri/excelize/v2" + +type Title struct { + // 标题名称 + Name string + // 标题位置(列) + Location Location + // 标题合并列数(除开本身) + Colspan int + // 标题合并行数(除开本身) + Rowspan int + Style *excelize.Style +} + +type Location struct { + X int + Y int +} + +func (e *Exporter) SetGlobalTitleStyle(style *excelize.Style) { + e.GlobalTitleStyle = style +} + +func (e *Exporter) SetTitle(sheet string) { + for _, title := range e.Titles { + start_col, end_col := e.CaculateCell(title.Location, title.Rowspan, title.Colspan) + e.xlsx.MergeCell(sheet, start_col, end_col) + e.xlsx.SetCellValue(sheet, start_col, title.Name) + if title.Style != nil { + style, _ := e.xlsx.NewStyle(title.Style) + e.xlsx.SetCellStyle(sheet, start_col, end_col, style) + } else if e.GlobalTitleStyle != nil { + style, _ := e.xlsx.NewStyle(e.GlobalTitleStyle) + e.xlsx.SetCellStyle(sheet, start_col, end_col, style) + + } + } +} diff --git a/export/xlsx.go b/export/xlsx.go new file mode 100644 index 0000000..a15ecf1 --- /dev/null +++ b/export/xlsx.go @@ -0,0 +1,132 @@ +package export + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/xuri/excelize/v2" +) + +type Exporter struct { + File string + Path string + Sheets []string + Titles []Title + Data interface{} + xlsx *excelize.File + GlobalTitleStyle *excelize.Style +} + +func NewExporter() *Exporter { + return &Exporter{ + xlsx: excelize.NewFile(), + } +} + +func DefaultExporter() *Exporter { + return &Exporter{ + xlsx: excelize.NewFile(), + GlobalTitleStyle: DefaultTitleStyle(), + Sheets: []string{"Sheet1"}, + } +} + +func (e *Exporter) newSheet() error { + for _, sheet := range e.Sheets { + _, err := e.xlsx.NewSheet(sheet) + if err != nil { + return fmt.Errorf("init sheet error:%s", err) + } + } + return nil +} + +func (e *Exporter) Export(sheetIndex int) error { + if len(e.Sheets) == 0 { + return fmt.Errorf("excel file has no sheet") + } + err := e.newSheet() + if err != nil { + return err + } + if len(e.Titles) == 0 { + return fmt.Errorf("excel title is null") + } + sheet := e.Sheets[sheetIndex] + e.SetTitle(sheet) + v := reflect.ValueOf(e.Data) + if v.Kind() != reflect.Pointer { + return fmt.Errorf("data must be pointer") + } + if v.Elem().Kind() != reflect.Slice { + return fmt.Errorf("data must be slice") + } + for i := 0; i < v.Elem().Len(); i++ { + value := v.Elem().Index(i) + if value.Kind() != reflect.Struct { + return fmt.Errorf("element must be struct") + } + e.dealElement(sheet, value, i) + } + return nil +} + +func (e *Exporter) dealElement(sheet string, data reflect.Value, index int) error { + for i := 0; i < data.NumField(); i++ { + field := data.Field(i) + switch field.Kind() { + case reflect.Struct: + e.dealElement(sheet, field, index) + // 该逻辑有待验证 + case reflect.Slice: + for j := 0; j < field.Len(); j++ { + value := field.Index(j) + if value.Kind() == reflect.Struct { + e.dealElement(sheet, value, index) + } + } + default: + t := data.Type().Field(i) + tag, ok := t.Tag.Lookup("export") + if ok { + tagMap := dealTag(tag) + x, _ := strconv.Atoi(tagMap["x"]) + y, _ := strconv.Atoi(tagMap["y"]) + colSpan, _ := strconv.Atoi(tagMap["colspan"]) + rowSpan, _ := strconv.Atoi(tagMap["rowspan"]) + location := Location{ + X: x, + Y: y + index, + } + cell := Cell{ + Location: location, + Rowspan: rowSpan, + Colspan: colSpan, + Value: field, + } + e.SetCell(sheet, cell) + } + } + + } + return e.Save() +} + +func dealTag(tag string) map[string]string { + tag_list := strings.Split(tag, ",") + tag_map := make(map[string]string) + for _, tag := range tag_list { + tag_kv := strings.Split(tag, ":") + if len(tag_kv) != 2 { + continue + } + tag_map[tag_kv[0]] = tag_kv[1] + } + return tag_map +} + +func (e *Exporter) Save() error { + return e.xlsx.SaveAs(fmt.Sprintf("%s/%s", e.Path, e.File)) +} diff --git a/export_test.go b/export_test.go new file mode 100644 index 0000000..5aa2cd5 --- /dev/null +++ b/export_test.go @@ -0,0 +1,119 @@ +package xlsx + +import ( + "testing" + + "git.botann.com/lijun/xlsx/export" +) + +type Fen int + +type Data struct { + Name string + TotalInvest Fen `export:"true,x:2,y:3"` + ProjectName string `export:"true,x:1,y:3"` + CenterInvest Fen + CityInvest Fen + CompanyInvest Fen + Partner string `export:"true,x:6,y:3"` + ExData []ExData + ExData2 ExData2 +} + +type ExData struct { + Col1 string + Col2 string +} + +type ExData2 struct { + CenterInvest Fen `export:"true,x:3,y:3"` + CityInvest Fen `export:"true,x:4,y:3"` + CompanyInvest Fen `export:"true,x:5,y:3"` +} + +func Test(t *testing.T) { + title := []export.Title{ + { + Name: "项目名称", + Location: export.Location{X: 1, Y: 1}, + Colspan: 1, + Rowspan: 0, + }, + { + Name: "总投资", + Location: export.Location{X: 2, Y: 1}, + Colspan: 1, + Rowspan: 0, + }, + { + Name: "资金来源", + Location: export.Location{X: 3, Y: 1}, + Colspan: 0, + Rowspan: 2, + }, + { + Name: "中央", + Location: export.Location{X: 3, Y: 2}, + Colspan: 0, + Rowspan: 0, + }, + { + Name: "地方", + Location: export.Location{X: 4, Y: 2}, + Colspan: 0, + Rowspan: 0, + }, + { + Name: "公司", + Location: export.Location{X: 5, Y: 2}, + Colspan: 0, + Rowspan: 0, + }, + { + Name: "合作主体", + Location: export.Location{X: 6, Y: 1}, + Colspan: 1, + Rowspan: 0, + }, + } + data := []Data{ + { + Name: "测试数据1", + TotalInvest: 10000, + ProjectName: "项目1", + CenterInvest: 2000, + CityInvest: 2000, + CompanyInvest: 6000, + Partner: "公司1", + ExData: []ExData{ + {Col1: "test1", Col2: "test2"}, + {Col1: "测试1", Col2: "测试2"}, + }, + ExData2: ExData2{ + CenterInvest: 3000, + CityInvest: 1500, + CompanyInvest: 5500, + }, + }, + { + Name: "测试数据", + TotalInvest: 8000, + ProjectName: "项目2", + CenterInvest: 2000, + CityInvest: 2000, + CompanyInvest: 4000, + Partner: "公司2", + ExData2: ExData2{ + CenterInvest: 6200, + CityInvest: 1200, + CompanyInvest: 2600, + }, + }, + } + exporter := export.DefaultExporter() + exporter.Data = &data + exporter.Titles = title + exporter.File = "test.xlsx" + exporter.Path = "./" + exporter.Export(0) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fbf2474 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.botann.com/lijun/xlsx + +go 1.21.5 + +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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..144577c --- /dev/null +++ b/go.sum @@ -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= diff --git a/test.xlsx b/test.xlsx new file mode 100755 index 0000000..3de3c61 Binary files /dev/null and b/test.xlsx differ