diff --git a/widgets/table/excel.go b/widgets/table/excel.go new file mode 100644 index 00000000..aa83fbc3 --- /dev/null +++ b/widgets/table/excel.go @@ -0,0 +1,116 @@ +package table + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/xuri/excelize/v2" + "github.com/yaoapp/kun/any" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/kun/maps" + "github.com/yaoapp/yao/xfs" +) + +// Export Export query result to Excel +func (dsl *DSL) Export(filename string, data interface{}, page int, chunkSize int) error { + + log.Trace("[Export] %s %d %d Before: %#v", filename, page, chunkSize, data) + + rows := []maps.MapStr{} + if values, ok := data.([]maps.MapStrAny); ok { + for _, row := range values { + rows = append(rows, row.Dot()) + } + } else if values, ok := data.([]map[string]interface{}); ok { + for _, row := range values { + rows = append(rows, maps.Of(row).Dot()) + } + } else if values, ok := data.([]interface{}); ok { + for _, row := range values { + rows = append(rows, any.Of(row).MapStr().Dot()) + } + } + + log.Trace("[Export] %s %d %d After: %#v", filename, page, chunkSize, data) + columns, err := dsl.exportSetting() + if err != nil { + return err + } + + if len(columns) == 0 { + return fmt.Errorf("the table does not support export") + } + + filename = filepath.Join(xfs.Stor.Root, filename) + if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) { + f := excelize.NewFile() + index := f.GetActiveSheetIndex() + name := f.GetSheetName(index) + f.SetSheetName(name, dsl.Name) + for i, column := range columns { + axis, err := excelize.CoordinatesToCellName(i+1, 1) + if err != nil { + return err + } + f.SetCellValue(dsl.Name, axis, column["name"]) + } + if err := f.SaveAs(filename); err != nil { + fmt.Println(err) + return err + } + } + + f, err := excelize.OpenFile(filename) + if err != nil { + return err + } + + defer f.Close() + offset := (page-1)*chunkSize + 2 + for line, row := range rows { + for i, column := range columns { + v := row.Get(column["field"]) + if v != nil { + axis, err := excelize.CoordinatesToCellName(i+1, line+offset) + if err != nil { + return err + } + f.SetCellValue(dsl.Name, axis, v) + } + } + // fmt.Println("--", line, page, offset, filename, chunkSize, "--") + } + + return f.Save() +} + +func (dsl *DSL) exportSetting() ([]map[string]string, error) { + // Validate params + if dsl.Layout == nil { + return nil, fmt.Errorf("the table layout does not found") + } + + if dsl.Fields == nil || dsl.Fields.Table == nil { + return nil, fmt.Errorf("the table fields does not found") + } + + if dsl.Layout.Table == nil || dsl.Layout.Table.Columns == nil { + return nil, fmt.Errorf("the columns table layout does not found") + } + + setting := []map[string]string{} + for _, column := range dsl.Layout.Table.Columns { + + if field, has := dsl.Fields.Table[column.Name]; has { + bind := field.Bind + if field.View != nil && field.View.Bind != "" { + bind = field.View.Bind + } + setting = append(setting, map[string]string{"name": column.Name, "field": bind}) + } + } + + return setting, nil +} diff --git a/widgets/table/process.go b/widgets/table/process.go index 4382c9f4..9db00f23 100644 --- a/widgets/table/process.go +++ b/widgets/table/process.go @@ -1,13 +1,20 @@ package table import ( + "crypto/md5" + "encoding/hex" "fmt" + "os" + "path/filepath" "strings" + "time" "github.com/yaoapp/gou" "github.com/yaoapp/gou/fs" + "github.com/yaoapp/kun/any" "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/log" + "github.com/yaoapp/kun/maps" "github.com/yaoapp/yao/helper" ) @@ -31,6 +38,7 @@ func exportProcess() { gou.RegisterProcessHandler("yao.table.delete", processDelete) gou.RegisterProcessHandler("yao.table.deletewhere", processDeleteWhere) gou.RegisterProcessHandler("yao.table.deletein", processDeleteIn) + gou.RegisterProcessHandler("yao.table.export", processExport) } func processXgen(process *gou.Process) interface{} { @@ -225,3 +233,66 @@ func processDeleteIn(process *gou.Process) interface{} { } return tab.Action.DeleteIn.MustExec(process) } + +// processExport yao.table.Export (:table, :queryParam, :chunkSize) +func processExport(process *gou.Process) interface{} { + process.ValidateArgNums(1) + tab := MustGet(process) // 0 + params := process.ArgsQueryParams(1, gou.QueryParam{}) + pagesize := process.ArgsInt(2, 50) + + // Filename + hash := md5.Sum([]byte(time.Now().Format("20060102-15:04:05"))) + fingerprint := string(hex.EncodeToString(hash[:])) + fingerprint = strings.ToUpper(fingerprint) + dir := time.Now().Format("20060102") + filename := filepath.Join(string(os.PathSeparator), dir, fmt.Sprintf("%s.xlsx", fingerprint)) + + // Create Data Path + fs := fs.MustGet("system") + if has, _ := fs.Exists(dir); !has { + fs.MkdirAll(dir, uint32(os.ModePerm)) + } + + // Query + page := 1 + for page > 0 { + process.Args = []interface{}{tab.ID, params, page, pagesize} + data, err := tab.Action.Search.Exec(process) + if err != nil { + log.Error("[table] export error %s", err.Error()) + page = -1 + continue + } + + res, ok := data.(map[string]interface{}) + if !ok { + res, ok = data.(maps.MapStrAny) + if !ok { + log.Error("[table] export Search Action response data error %#v", data) + page = -1 + continue + } + } + + if _, ok := res["next"]; !ok { + page = -1 + continue + } + + size := pagesize + if _, ok := res["pagesize"]; ok { + size = any.Of(res["pagesize"]).CInt() + } + + // Export + err = tab.Export(filename, res["data"], page, size) + if err != nil { + log.Error("Export %s %s", tab.ID, err.Error()) + } + + page = any.Of(res["next"]).CInt() + } + + return filename +} diff --git a/widgets/table/process_test.go b/widgets/table/process_test.go index b5e8eb51..5df0aa87 100644 --- a/widgets/table/process_test.go +++ b/widgets/table/process_test.go @@ -432,6 +432,18 @@ func TestProcessXgen(t *testing.T) { } +func TestProcessExport(t *testing.T) { + load(t) + clear(t) + testData(t) + args := []interface{}{"pet", gou.QueryParam{Wheres: []gou.QueryWhere{{Column: "mode", Value: "enabled"}}}, 2} + response := gou.NewProcess("yao.table.Export", args...).Run() + assert.NotNil(t, response) + fs := fs.MustGet("system") + size, _ := fs.Size(response.(string)) + assert.Greater(t, size, 1000) +} + func load(t *testing.T) { prepare(t) err := Load(config.Conf)