diff --git a/Makefile b/Makefile index b144596a..9c0b5f1b 100644 --- a/Makefile +++ b/Makefile @@ -205,7 +205,7 @@ release: clean # Making artifacts mkdir -p dist - CGO_ENABLED=1 go build -v -o dist/release/yao + CGO_ENABLED=1 CGO_LDFLAGS="-static" go build -v -o dist/release/yao chmod +x dist/release/yao # make clean diff --git a/share/types.go b/share/types.go index 2f6253c9..60ec16a9 100644 --- a/share/types.go +++ b/share/types.go @@ -27,10 +27,11 @@ type ColumnImp Column // Column 字段呈现方式 type Column struct { - Label string `json:"label"` - View Render `json:"view,omitempty"` - Edit Render `json:"edit,omitempty"` - Form Render `json:"form,omitempty"` + Label string `json:"label"` + Export string `json:"export,omitempty"` + View Render `json:"view,omitempty"` + Edit Render `json:"edit,omitempty"` + Form Render `json:"form,omitempty"` Importable } diff --git a/table/process.go b/table/process.go index 063f9860..a6154d5e 100644 --- a/table/process.go +++ b/table/process.go @@ -1,12 +1,19 @@ package table import ( + "crypto/md5" + "encoding/hex" + "fmt" + "os" + "path/filepath" "strings" + "time" "github.com/yaoapp/gou" "github.com/yaoapp/kun/any" "github.com/yaoapp/kun/log" "github.com/yaoapp/kun/maps" + "github.com/yaoapp/yao/xfs" ) func init() { @@ -22,6 +29,7 @@ func init() { gou.RegisterProcessHandler("xiang.table.QuickSave", ProcessQuickSave) gou.RegisterProcessHandler("xiang.table.UpdateIn", ProcessUpdateIn) gou.RegisterProcessHandler("xiang.table.DeleteIn", ProcessDeleteIn) + gou.RegisterProcessHandler("xiang.table.Export", ProcessExport) gou.RegisterProcessHandler("xiang.table.Setting", ProcessSetting) } @@ -36,10 +44,6 @@ func ProcessSearch(process *gou.Process) interface{} { api := table.APIs["search"].ValidateLoop("xiang.table.search") - // if process.NumOfArgsIs(5) && api.IsAllow(process.Args[4]) { - // return nil - // } - // Before Hook process.Args = table.Before(table.Hooks.BeforeSearch, process.Args, process.Sid) @@ -329,3 +333,69 @@ func ProcessSelect(process *gou.Process) interface{} { // After Hook return table.After(table.Hooks.AfterSelect, response, process.Args[1:], process.Sid) } + +// ProcessExport xiang.table.Export (:table, :queryParam, :chunkSize) +// Export query result to Excel +func ProcessExport(process *gou.Process) interface{} { + + process.ValidateArgNums(1) + name := process.ArgsString(0) + table := Select(name) + api := table.APIs["search"].ValidateLoop("xiang.table.search") + + // Make DIR + 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") + ext := filepath.Ext("export.xlsx") + filename := filepath.Join(dir, fmt.Sprintf("%s%s", fingerprint, ext)) + xfs.Stor.MustMkdirAll(dir, os.ModePerm) + + page := 1 + for page > 0 { + + // Before Hook + process.Args = table.Before(table.Hooks.BeforeSearch, process.Args, process.Sid) + + // 参数表 + process.ValidateArgNums(3) + param := api.MergeDefaultQueryParam(process.ArgsQueryParams(1), 0, process.Sid) + pagesize := process.ArgsInt(2, api.DefaultInt(2)) + if process.NumOfArgs() == 4 { // for search hook + pagesize = process.ArgsInt(3, api.DefaultInt(1)) + } + + // 查询数据 + response := gou.NewProcess(api.Process, param, page, pagesize). + WithGlobal(process.Global). + WithSID(process.Sid). + Run() + + // After Hook + response = table.After(table.Hooks.AfterSearch, response, []interface{}{param, page, pagesize}, process.Sid) + + res, ok := response.(map[string]interface{}) + if !ok { + res, ok = response.(maps.MapStrAny) + if !ok { + page = -1 + continue + } + } + + if _, ok := res["next"]; !ok { + page = -1 + continue + } + + err := table.Export(filename, res["data"], page, pagesize) + if err != nil { + log.Error("Export %s %s", table.Table, err.Error()) + } + + page = any.Of(res["next"]).CInt() + } + + return filename +} diff --git a/table/process_test.go b/table/process_test.go index 7fddf946..c984fbb0 100644 --- a/table/process_test.go +++ b/table/process_test.go @@ -467,3 +467,44 @@ func TestTableProcessSelectWithHook(t *testing.T) { // 清空数据 capsule.Query().Table("service").WhereIn("id", []int{id}).Delete() } + +func TestTableProcessExport(t *testing.T) { + + args := []interface{}{ + "service", + gou.QueryParam{Wheres: []gou.QueryWhere{{Column: "status", Value: "enabled"}}}, + 2, + } + response := gou.NewProcess("xiang.table.Export", args...).Run() + assert.NotNil(t, response) + // fmt.Println(response) + // res := any.Of(response).Map() + // assert.True(t, res.Has("data")) + // assert.True(t, res.Has("next")) + // assert.True(t, res.Has("page")) + // assert.True(t, res.Has("pagecnt")) + // assert.True(t, res.Has("pagesize")) + // assert.True(t, res.Has("prev")) + // assert.True(t, res.Has("total")) + // assert.Equal(t, 1, res.Get("page")) + // assert.Equal(t, 2, res.Get("pagesize")) +} + +func TestTableProcessExportWithHook(t *testing.T) { + + args := []interface{}{"hooks.search"} + response := gou.NewProcess("xiang.table.Export", args...).Run() + + assert.NotNil(t, response) + // res := any.Of(response).Map() + // assert.True(t, res.Has("data")) + // assert.True(t, res.Has("next")) + // assert.True(t, res.Has("page")) + // assert.True(t, res.Has("pagecnt")) + // assert.True(t, res.Has("pagesize")) + // assert.True(t, res.Has("prev")) + // assert.True(t, res.Has("total")) + // assert.Equal(t, 1, res.Get("page")) + // assert.Equal(t, 2, res.Get("pagesize")) + // assert.Equal(t, float64(100), res.Get("after")) +} diff --git a/table/table.go b/table/table.go index 4121c3a5..cc6ee973 100644 --- a/table/table.go +++ b/table/table.go @@ -1,6 +1,7 @@ package table import ( + "errors" "fmt" "io" "os" @@ -8,12 +9,15 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/xuri/excelize/v2" "github.com/yaoapp/gou" "github.com/yaoapp/gou/helper" "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/log" + "github.com/yaoapp/kun/maps" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/share" + "github.com/yaoapp/yao/xfs" ) // Tables 已载入模型 @@ -286,3 +290,124 @@ func (table *Table) loadColumns() { } table.Columns = defaults } + +// Export Export query result to Excel +func (table *Table) Export(filename string, data interface{}, page int, chunkSize int) error { + + 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()) + } + } + + columns, err := table.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, table.Name) + for i, column := range columns { + axis, err := excelize.CoordinatesToCellName(i+1, 1) + if err != nil { + return err + } + f.SetCellValue(table.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(table.Name, axis, v) + } + } + // fmt.Println("--", line, page, offset, "--") + } + + return f.Save() +} + +func (table *Table) exportSetting() ([]map[string]string, error) { + // Validate params + if table.List.Layout == nil { + return nil, fmt.Errorf("the table layout does not found") + } + + columns, has := table.List.Layout["columns"] + if !has { + return nil, fmt.Errorf("the columns table layout does not found") + } + + fields, ok := columns.([]interface{}) + if !ok { + return nil, fmt.Errorf("Table Layout columns format error") + } + + setting := []map[string]string{} + for _, field := range fields { + f, ok := field.(map[string]interface{}) + if !ok { + continue + } + + n, has := f["name"] + if !has { + continue + } + + name, ok := n.(string) + if !ok { + continue + } + + column, has := table.Columns[name] + if !has { + continue + } + + field := column.Export + if field == "" { + if value, has := column.View.Props["value"]; has { + if valueStr, ok := value.(string); ok { + field = strings.TrimPrefix(valueStr, ":") + } + } + } + + if field != "" && name != "" { + setting = append(setting, map[string]string{"name": name, "field": field}) + } + } + + return setting, nil +} diff --git a/tests/tables/hooks/search.tab.json b/tests/tables/hooks/search.tab.json index d38f27b5..46f6a8b8 100644 --- a/tests/tables/hooks/search.tab.json +++ b/tests/tables/hooks/search.tab.json @@ -22,7 +22,7 @@ "list": { "primary": "id", "layout": { - "columns": [{ "name": "ID", "width": 6 }], + "columns": [{ "name": "id", "width": 6 }, { "name": "城市" }], "filters": [] }, "actions": {} diff --git a/tests/tables/service.json b/tests/tables/service.json index 871bc495..641d9a6b 100644 --- a/tests/tables/service.json +++ b/tests/tables/service.json @@ -316,11 +316,13 @@ "primary": "id", "layout": { "columns": [ + { "name": "id", "width": 6 }, { "name": "服务名称", "width": 6 }, { "name": "所属厂商", "width": 6 }, { "name": "服务类型", "width": 4 }, { "name": "状态", "width": 4 }, { "name": "服务领域", "width": 4 }, + { "name": "行业覆盖", "width": 4 }, { "name": "计费方式", "width": 4 }, { "name": "创建时间", "width": 6 }, { "name": "更新时间", "width": 6 }