[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
|
# Making artifacts
|
||||||
mkdir -p dist
|
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
|
chmod +x dist/release/yao
|
||||||
|
|
||||||
# make clean
|
# make clean
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type ColumnImp Column
|
||||||
// Column 字段呈现方式
|
// Column 字段呈现方式
|
||||||
type Column struct {
|
type Column struct {
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
|
Export string `json:"export,omitempty"`
|
||||||
View Render `json:"view,omitempty"`
|
View Render `json:"view,omitempty"`
|
||||||
Edit Render `json:"edit,omitempty"`
|
Edit Render `json:"edit,omitempty"`
|
||||||
Form Render `json:"form,omitempty"`
|
Form Render `json:"form,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
package table
|
package table
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/yaoapp/gou"
|
"github.com/yaoapp/gou"
|
||||||
"github.com/yaoapp/kun/any"
|
"github.com/yaoapp/kun/any"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/kun/maps"
|
"github.com/yaoapp/kun/maps"
|
||||||
|
"github.com/yaoapp/yao/xfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -22,6 +29,7 @@ func init() {
|
||||||
gou.RegisterProcessHandler("xiang.table.QuickSave", ProcessQuickSave)
|
gou.RegisterProcessHandler("xiang.table.QuickSave", ProcessQuickSave)
|
||||||
gou.RegisterProcessHandler("xiang.table.UpdateIn", ProcessUpdateIn)
|
gou.RegisterProcessHandler("xiang.table.UpdateIn", ProcessUpdateIn)
|
||||||
gou.RegisterProcessHandler("xiang.table.DeleteIn", ProcessDeleteIn)
|
gou.RegisterProcessHandler("xiang.table.DeleteIn", ProcessDeleteIn)
|
||||||
|
gou.RegisterProcessHandler("xiang.table.Export", ProcessExport)
|
||||||
gou.RegisterProcessHandler("xiang.table.Setting", ProcessSetting)
|
gou.RegisterProcessHandler("xiang.table.Setting", ProcessSetting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,10 +44,6 @@ func ProcessSearch(process *gou.Process) interface{} {
|
||||||
|
|
||||||
api := table.APIs["search"].ValidateLoop("xiang.table.search")
|
api := table.APIs["search"].ValidateLoop("xiang.table.search")
|
||||||
|
|
||||||
// if process.NumOfArgsIs(5) && api.IsAllow(process.Args[4]) {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Before Hook
|
// Before Hook
|
||||||
process.Args = table.Before(table.Hooks.BeforeSearch, process.Args, process.Sid)
|
process.Args = table.Before(table.Hooks.BeforeSearch, process.Args, process.Sid)
|
||||||
|
|
||||||
|
|
@ -329,3 +333,69 @@ func ProcessSelect(process *gou.Process) interface{} {
|
||||||
// After Hook
|
// After Hook
|
||||||
return table.After(table.Hooks.AfterSelect, response, process.Args[1:], process.Sid)
|
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()
|
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
|
package table
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -8,12 +9,15 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
"github.com/yaoapp/gou"
|
"github.com/yaoapp/gou"
|
||||||
"github.com/yaoapp/gou/helper"
|
"github.com/yaoapp/gou/helper"
|
||||||
"github.com/yaoapp/kun/exception"
|
"github.com/yaoapp/kun/exception"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
|
"github.com/yaoapp/kun/maps"
|
||||||
"github.com/yaoapp/yao/config"
|
"github.com/yaoapp/yao/config"
|
||||||
"github.com/yaoapp/yao/share"
|
"github.com/yaoapp/yao/share"
|
||||||
|
"github.com/yaoapp/yao/xfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tables 已载入模型
|
// Tables 已载入模型
|
||||||
|
|
@ -286,3 +290,124 @@ func (table *Table) loadColumns() {
|
||||||
}
|
}
|
||||||
table.Columns = defaults
|
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": {
|
"list": {
|
||||||
"primary": "id",
|
"primary": "id",
|
||||||
"layout": {
|
"layout": {
|
||||||
"columns": [{ "name": "ID", "width": 6 }],
|
"columns": [{ "name": "id", "width": 6 }, { "name": "城市" }],
|
||||||
"filters": []
|
"filters": []
|
||||||
},
|
},
|
||||||
"actions": {}
|
"actions": {}
|
||||||
|
|
|
||||||
|
|
@ -316,11 +316,13 @@
|
||||||
"primary": "id",
|
"primary": "id",
|
||||||
"layout": {
|
"layout": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
{ "name": "id", "width": 6 },
|
||||||
{ "name": "服务名称", "width": 6 },
|
{ "name": "服务名称", "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": 4 },
|
||||||
|
{ "name": "行业覆盖", "width": 4 },
|
||||||
{ "name": "计费方式", "width": 4 },
|
{ "name": "计费方式", "width": 4 },
|
||||||
{ "name": "创建时间", "width": 6 },
|
{ "name": "创建时间", "width": 6 },
|
||||||
{ "name": "更新时间", "width": 6 }
|
{ "name": "更新时间", "width": 6 }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue