[add] xiang.table.Export process

This commit is contained in:
Max 2022-04-14 15:54:04 +08:00
parent 8bd280bb6b
commit af639688df
7 changed files with 249 additions and 10 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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"))
}

View file

@ -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
}

View file

@ -22,7 +22,7 @@
"list": {
"primary": "id",
"layout": {
"columns": [{ "name": "ID", "width": 6 }],
"columns": [{ "name": "id", "width": 6 }, { "name": "城市" }],
"filters": []
},
"actions": {}

View file

@ -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 }