From 8552e651abfe4d4b24eddbdc7d339309bbd1997c Mon Sep 17 00:00:00 2001 From: gogacoder Date: Wed, 8 Jan 2025 22:52:06 +0700 Subject: [PATCH] feat: excel export (only primitive types) --- .../app/internal/services/postservice.ts | 5 + frontend/src/components/HelloWorld.vue | 1 + internal/extras/excel/export.go | 190 +++++++++++++++++- internal/models/models.go | 4 +- internal/services/post.go | 10 + 5 files changed, 204 insertions(+), 6 deletions(-) diff --git a/frontend/bindings/app/internal/services/postservice.ts b/frontend/bindings/app/internal/services/postservice.ts index 1813426..1c76553 100644 --- a/frontend/bindings/app/internal/services/postservice.ts +++ b/frontend/bindings/app/internal/services/postservice.ts @@ -36,6 +36,11 @@ export function Delete(item: $models.Post): Promise<$models.Post> & { cancel(): return $typingPromise; } +export function ExportToExcel(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(75322242) as any; + return $resultPromise; +} + export function GetAll(): Promise<($models.Post | null)[]> & { cancel(): void } { let $resultPromise = $Call.ByID(65691059) as any; let $typingPromise = $resultPromise.then(($result) => { diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue index dd639de..1fc7755 100644 --- a/frontend/src/components/HelloWorld.vue +++ b/frontend/src/components/HelloWorld.vue @@ -16,6 +16,7 @@ const doGreet = () => { onMounted(async () => { console.log(await PostService.GetById(5)) + await PostService.ExportToExcel() }) diff --git a/internal/extras/excel/export.go b/internal/extras/excel/export.go index 0efe8ea..0cc1f7e 100644 --- a/internal/extras/excel/export.go +++ b/internal/extras/excel/export.go @@ -1,18 +1,33 @@ package excel import ( - "app/internal/services" + "app/internal/dialogs" + "fmt" "github.com/xuri/excelize/v2" "log/slog" "reflect" + "slices" ) type TableHeaders struct { Headers []string - IgnoredFieldsIndices []uint + IgnoredFieldsIndices []int } -func ExportEntityToSpreadsheet[T any](filename, sheetName string, entity T, service services.Service[T]) error { +func isPrimitive(valueType reflect.Type) bool { + switch valueType.Kind() { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, + reflect.String: + return true + default: + return false + } +} + +func ExportEntityToSpreadsheet[T any](filename, sheetName string, entity T, provider func() ([]*T, error)) error { file := excelize.NewFile() defer func() { if err := file.Close(); err != nil { @@ -20,9 +35,86 @@ func ExportEntityToSpreadsheet[T any](filename, sheetName string, entity T, serv } }() + if _, err := file.NewSheet(sheetName); err != nil { + return err + } + if err := file.DeleteSheet("Sheet1"); err != nil { + return err + } + + headers, err := WriteHeaders(sheetName, entity, file) + if err != nil { + return err + } + + items, err := provider() + if err != nil { + return err + } + + // TODO: process composite objects + // TODO: appearance + for i, item := range items { + structValue := reflect.ValueOf(item).Elem() + for j := range structValue.NumField() { + if slices.Contains(headers.IgnoredFieldsIndices, j) { + continue + } + field := structValue.Field(j) + + if isPrimitive(field.Type()) { + fieldValue := reflect.ValueOf(field) + + cellName, err := GetCellNameByIndices(j, i+1) + if err != nil { + return err + } + fmt.Printf("Field %s value: %v\n", cellName, fieldValue.Interface()) + + err = file.SetCellValue(sheetName, cellName, fieldValue.Interface()) + if err != nil { + return err + } + } + } + } + + filepath, err := dialogs.SaveFileDialog("Экспорт данных", filename) + if err != nil { + return err + } + + if err := file.SaveAs(filepath); err != nil { + return err + } + return nil } +func GetHeaderCellNameByIndex(column int) (string, error) { + colName, err := excelize.ColumnNumberToName(column + 1) + if err != nil { + return "", err + } + cellName, err := excelize.JoinCellName(colName, 1) + if err != nil { + return "", err + } + return cellName, nil +} + +func GetCellNameByIndices(column int, row int) (string, error) { + colName, err := excelize.ColumnNumberToName(column + 1) + if err != nil { + return "", err + } + cellName, err := excelize.JoinCellName(colName, row+1) + if err != nil { + return "", err + } + return cellName, nil +} + func ExportHeaders(entity any) TableHeaders { headers := TableHeaders{} v := reflect.TypeOf(entity) @@ -30,8 +122,98 @@ func ExportHeaders(entity any) TableHeaders { field := v.Field(i) displayName := field.Tag.Get("displayName") if displayName != "" { - + headers.Headers = append(headers.Headers, displayName) + } else { + headers.IgnoredFieldsIndices = append(headers.IgnoredFieldsIndices, i) } } return headers } + +func WriteHeaders(sheetName string, entity any, file *excelize.File) (TableHeaders, error) { + headers := ExportHeaders(entity) + for i, header := range headers.Headers { + cellName, err := GetHeaderCellNameByIndex(i) + if err != nil { + return headers, err + } + + err = file.SetCellValue(sheetName, cellName, header) + if err != nil { + return headers, err + } + } + err := ApplyStyleHeaders(file, sheetName, headers) + if err != nil { + return headers, err + } + return headers, nil +} + +func GetStyleId(f *excelize.File, style *excelize.Style) (int, error) { + styleId, err := f.NewStyle(style) + if err != nil { + return 0, fmt.Errorf("ошибка при создании стиля: %w", err) + } + + return styleId, nil +} + +func LoadHeadersStyle(file *excelize.File) (int, error) { + headersStyle := excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + }, + Border: []excelize.Border{ + { + Type: "left", + Color: "000000", + Style: 1, + }, + { + Type: "right", + Color: "000000", + Style: 1, + }, + { + Type: "top", + Color: "000000", + Style: 1, + }, + { + Type: "bottom", + Color: "000000", + Style: 1, + }, + }, + Font: &excelize.Font{ + Bold: true, + VertAlign: "center", + }, + } + return GetStyleId(file, &headersStyle) +} + +func ApplyStyleHeaders(file *excelize.File, sheetName string, headers TableHeaders) error { + styleId, err := LoadHeadersStyle(file) + if err != nil { + return err + } + + cellName, err := GetHeaderCellNameByIndex(len(headers.Headers) - 1) + if err != nil { + return err + } + + err = file.SetCellStyle(sheetName, "A1", cellName, styleId) + if err != nil { + return err + } + + return nil +} + +func WriteData(file *excelize.File) { + +} diff --git a/internal/models/models.go b/internal/models/models.go index 42c1ed4..9d78350 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -5,8 +5,8 @@ var Entities = []any{ } type Post struct { - Id uint `gorm:"primaryKey"` - Text string `displayName:"Текст"` + Id uint `gorm:"primaryKey" displayName:"Номер"` + Text string `displayName:"Текст" displayName:"Текст поста"` CreatedAt int64 `gorm:"autoCreateTime" displayName:"Дата публикации" cellType:"timestamp"` } diff --git a/internal/services/post.go b/internal/services/post.go index d03d443..6df89b5 100644 --- a/internal/services/post.go +++ b/internal/services/post.go @@ -2,8 +2,11 @@ package services import ( "app/internal/dal" + "app/internal/dialogs" + excel "app/internal/extras/excel" "app/internal/models" "errors" + "fmt" "gorm.io/gen/field" "gorm.io/gorm" ) @@ -43,3 +46,10 @@ func (service *PostService) Count() (int64, error) { amount, err := dal.Post.Count() return amount, err } + +func (service *PostService) ExportToExcel() { + err := excel.ExportEntityToSpreadsheet("report.xlsx", "Посты", Post{}, service.GetAll) + if err != nil { + dialogs.ErrorDialog("Ошибка экспорта", fmt.Sprintf("Ошибка при экспорте данных: %s", err)) + } +}