[add] xiang.table.Export process
This commit is contained in:
parent
8bd280bb6b
commit
af639688df
7 changed files with 249 additions and 10 deletions
2
Makefile
2
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
125
table/table.go
125
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
"list": {
|
||||
"primary": "id",
|
||||
"layout": {
|
||||
"columns": [{ "name": "ID", "width": 6 }],
|
||||
"columns": [{ "name": "id", "width": 6 }, { "name": "城市" }],
|
||||
"filters": []
|
||||
},
|
||||
"actions": {}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue