From 42262ce799a79fb43fcaccdc73695f90c745bc3f Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 6 Nov 2021 15:11:53 +0800 Subject: [PATCH] =?UTF-8?q?+=20=E5=85=B1=E4=BA=AB=E5=BA=93=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.go | 6 + engine/load.go | 1 + model/model_test.go | 2 +- share/importable.go | 158 +++++++++++++++++ share/importable_test.go | 57 ++++++ share/types.go | 40 ++++- table/process_test.go | 1 + table/table_test.go | 3 +- tests/libs/action.json | 14 ++ tests/libs/apis/table.json | 5 + tests/libs/column.json | 32 ++++ tests/libs/filter.json | 47 +++++ tests/libs/pages/static.json | 16 ++ tests/libs/render.json | 10 ++ tests/models/customer.mod.json | 230 +++++++++++++++++++++++++ tests/models/service.json | 1 + tests/tables/importable/table.tab.json | 75 ++++++++ 17 files changed, 689 insertions(+), 9 deletions(-) create mode 100644 share/importable.go create mode 100644 share/importable_test.go create mode 100644 tests/libs/action.json create mode 100644 tests/libs/apis/table.json create mode 100644 tests/libs/column.json create mode 100644 tests/libs/filter.json create mode 100644 tests/libs/pages/static.json create mode 100644 tests/libs/render.json create mode 100644 tests/models/customer.mod.json create mode 100644 tests/tables/importable/table.tab.json diff --git a/config/config.go b/config/config.go index d75336d3..3b8b5688 100644 --- a/config/config.go +++ b/config/config.go @@ -38,6 +38,7 @@ type XiangConfig struct { RootModel string `json:"root_model,omitempty" env:"XIANG_ROOT_MODEL"` // 应用模型文件目录 RootFLow string `json:"root_flow,omitempty" env:"XIANG_ROOT_FLOW"` // 应用业务逻辑文件目录 RootPlugin string `json:"root_plugin,omitempty" env:"XIANG_ROOT_PLUGIN"` // 应用业务插件文件目录 + RootLib string `json:"root_lib,omitempty" env:"XIANG_ROOT_LIB"` // 应用资料库文件目录 RootTable string `json:"root_table,omitempty" env:"XIANG_ROOT_TABLE"` // 应用数据表格文件目录 RootChart string `json:"root_chart,omitempty" env:"XIANG_ROOT_CHART"` // 应用分析图表文件目录 RootPage string `json:"root_page,omitempty" env:"XIANG_ROOT_PAGE"` // 应用通用页面文件目录 @@ -148,6 +149,11 @@ func (cfg *Config) SetDefaults() { if cfg.RootTable == "" { cfg.RootTable = cfg.Root + "/tables" } + + if cfg.RootLib == "" { + cfg.RootLib = cfg.Root + "/libs" + } + if cfg.RootChart == "" { cfg.RootChart = cfg.Root + "/charts" } diff --git a/engine/load.go b/engine/load.go index 33888184..94ac9340 100644 --- a/engine/load.go +++ b/engine/load.go @@ -28,6 +28,7 @@ func Load(cfg config.Config) { LoadEngine(cfg.Path) query.Load(cfg) // 加载数据分析引擎 + share.Load(cfg) // 加载共享库 lib model.Load(cfg) // 加载数据模型 model api.Load(cfg) // 加载业务接口 API flow.Load(cfg) // 加载业务逻辑 Flow diff --git a/model/model_test.go b/model/model_test.go index d721d9a2..2bc5ab49 100644 --- a/model/model_test.go +++ b/model/model_test.go @@ -23,5 +23,5 @@ func check(t *testing.T) { for key := range gou.Models { keys = append(keys, key) } - assert.Equal(t, 9, len(keys)) + assert.Equal(t, 10, len(keys)) } diff --git a/share/importable.go b/share/importable.go new file mode 100644 index 00000000..0d7e6ff1 --- /dev/null +++ b/share/importable.go @@ -0,0 +1,158 @@ +package share + +import ( + "fmt" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/query/share" + "github.com/yaoapp/kun/exception" + "github.com/yaoapp/kun/maps" + "github.com/yaoapp/xiang/config" +) + +// Libs 共享库 +var Libs = map[string]map[string]interface{}{} + +// Load 加载共享库 +func Load(cfg config.Config) { + LoadFrom(cfg.RootLib) +} + +// LoadFrom 从特定目录加载共享库 +func LoadFrom(dir string) { + + if DirNotExists(dir) { + return + } + Walk(dir, ".json", func(root, filename string) { + name := SpecName(root, filename) + content := ReadFile(filename) + libs := map[string]map[string]interface{}{} + err := jsoniter.Unmarshal(content, &libs) + if err != nil { + exception.New("共享数据结构异常 %s", 400, err).Throw() + } + for key, lib := range libs { + key := fmt.Sprintf("%s.%s", name, key) + Libs[key] = lib + // 删除注释 + if _, has := lib["__comment"]; has { + delete(lib, "__comment") + } + } + }) +} + +// UnmarshalJSON Column 字段JSON解析 +func (col *Column) UnmarshalJSON(data []byte) error { + new := ColumnImp{} + err := jsoniter.Unmarshal(data, &new) + if err != nil { + return err + } + + // 导入 + err = ImportJSON(new.Import, new.In, &new) + if err != nil { + return err + } + + *col = Column(new) + return nil +} + +// UnmarshalJSON Filter 字段JSON解析 +func (filter *Filter) UnmarshalJSON(data []byte) error { + new := FilterImp{} + err := jsoniter.Unmarshal(data, &new) + if err != nil { + return err + } + + // 导入 + err = ImportJSON(new.Import, new.In, &new) + if err != nil { + return err + } + + *filter = Filter(new) + return nil +} + +// UnmarshalJSON Render 字段JSON解析 +func (render *Render) UnmarshalJSON(data []byte) error { + new := RenderImp{} + err := jsoniter.Unmarshal(data, &new) + if err != nil { + return err + } + + // 导入 + err = ImportJSON(new.Import, new.In, &new) + if err != nil { + return err + } + + *render = Render(new) + return nil +} + +// UnmarshalJSON Page 字段JSON解析 +func (page *Page) UnmarshalJSON(data []byte) error { + new := PageImp{} + err := jsoniter.Unmarshal(data, &new) + if err != nil { + return err + } + + // 导入 + err = ImportJSON(new.Import, new.In, &new) + if err != nil { + return err + } + + *page = Page(new) + return nil +} + +// UnmarshalJSON API 字段JSON解析 +func (api *API) UnmarshalJSON(data []byte) error { + new := APIImp{} + err := jsoniter.Unmarshal(data, &new) + if err != nil { + return err + } + + // 导入 + err = ImportJSON(new.Import, new.In, &new) + if err != nil { + return err + } + + *api = API(new) + return nil +} + +// ImportJSON 导入 +func ImportJSON(name string, in []interface{}, v interface{}) error { + if name == "" { + return nil + } + + lib, has := Libs[name] + if !has { + return fmt.Errorf("共享库 %s 不存在", name) + } + + data := maps.MapStrAny{"$in": in}.Dot() + content, err := jsoniter.Marshal(share.Bind(lib, data)) + if err != nil { + return err + } + + err = jsoniter.Unmarshal(content, v) + if err != nil { + return err + } + return nil +} diff --git a/share/importable_test.go b/share/importable_test.go new file mode 100644 index 00000000..ad8543bc --- /dev/null +++ b/share/importable_test.go @@ -0,0 +1,57 @@ +package share + +import ( + "path" + "testing" + + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" + "github.com/yaoapp/xiang/config" +) + +func init() { + rootLib := path.Join(config.Conf.Source, "/tests/libs") + LoadFrom(rootLib) +} + +func TestColumn(t *testing.T) { + content := `{ "@": "column.Image", "in": ["LOGO", ":logo", 40] }` + column := Column{} + jsoniter.Unmarshal([]byte(content), &column) + assert.Equal(t, "upload", column.Edit.Type) + assert.Equal(t, ":logo", column.Edit.Props["value"]) + assert.Equal(t, "image", column.View.Type) + assert.Equal(t, float64(40), column.View.Props["height"]) + assert.Equal(t, float64(40), column.View.Props["width"]) + assert.Equal(t, ":logo", column.View.Props["value"]) +} + +func TestFilter(t *testing.T) { + content := `{ "@": "filter.关键词", "in": ["where.name.match"] }` + filter := Filter{} + jsoniter.Unmarshal([]byte(content), &filter) + assert.Equal(t, "where.name.match", filter.Bind) +} + +func TestRender(t *testing.T) { + content := `{ "@": "render.Image", "in": [":image", 40, 60] }` + render := Render{} + jsoniter.Unmarshal([]byte(content), &render) + assert.Equal(t, ":image", render.Props["value"]) + assert.Equal(t, float64(40), render.Props["width"]) + assert.Equal(t, float64(60), render.Props["height"]) +} + +func TestPage(t *testing.T) { + content := `{ "@": "pages.static.Page", "in": ["id"] }` + page := Page{} + jsoniter.Unmarshal([]byte(content), &page) + assert.Equal(t, "id", page.Primary) +} + +func TestAPI(t *testing.T) { + content := `{ "@": "apis.table.Search", "in": [10] }` + api := API{} + jsoniter.Unmarshal([]byte(content), &api) + assert.Equal(t, []interface{}{nil, nil, float64(10)}, api.Default) +} diff --git a/share/types.go b/share/types.go index a5858334..e854f589 100644 --- a/share/types.go +++ b/share/types.go @@ -2,6 +2,15 @@ package share import "github.com/yaoapp/kun/maps" +// Importable 可导入JSON +type Importable struct { + Import string `json:"@,omitempty"` // 从 Global 或 Vendor 载入 + In []interface{} `json:"in,omitempty"` // 从 Global 或 Vendor 载入, 解析参数 +} + +// APIImp 导入配置数据结构 +type APIImp API + // API API 配置数据结构 type API struct { Name string `json:"-"` @@ -10,36 +19,53 @@ type API struct { Process string `json:"process,omitempty"` Guard string `json:"guard,omitempty"` Default []interface{} `json:"default,omitempty"` + Importable } +// ColumnImp 导入模式查询过滤器 +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"` + Importable } +// FilterImp 导入模式查询过滤器 +type FilterImp Filter + // Filter 查询过滤器 type Filter struct { Label string `json:"label"` Bind string `json:"bind,omitempty"` Input Render `json:"input,omitempty"` + Importable } +// RenderImp 导入模式组件渲染方式 +type RenderImp Render + +// Render 组件渲染方式 +type Render struct { + Type string `json:"type,omitempty"` + Props map[string]interface{} `json:"props,omitempty"` + Components map[string]interface{} `json:"components,omitempty"` + Importable +} + +// PageImp 导入模式页面 +type PageImp Page + // Page 页面 type Page struct { Primary string `json:"primary"` Layout map[string]interface{} `json:"layout"` Actions map[string]Render `json:"actions,omitempty"` Option map[string]interface{} `json:"option,omitempty"` -} - -// Render 组件渲染方式 -type Render struct { - Type string `json:"type,omitempty"` - Props map[string]interface{} `json:"props,omitempty"` - Components map[string]interface{} `json:"components,omitempty"` + Importable } // AppInfo 应用信息 diff --git a/table/process_test.go b/table/process_test.go index c1596bc3..89f4bdaf 100644 --- a/table/process_test.go +++ b/table/process_test.go @@ -16,6 +16,7 @@ import ( func init() { share.DBConnect(config.Conf.Database) model.Load(config.Conf) + share.Load(config.Conf) Load(config.Conf) } func TestTableProcessSearch(t *testing.T) { diff --git a/table/table_test.go b/table/table_test.go index 5bffcf99..11b18612 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -12,6 +12,7 @@ import ( func TestLoad(t *testing.T) { share.DBConnect(config.Conf.Database) + share.Load(config.Conf) model.Load(config.Conf) Tables = make(map[string]*Table) @@ -47,5 +48,5 @@ func check(t *testing.T) { for key := range Tables { keys = append(keys, key) } - assert.Equal(t, 2, len(keys)) + assert.Equal(t, 3, len(keys)) } diff --git a/tests/libs/action.json b/tests/libs/action.json new file mode 100644 index 00000000..0e9348ad --- /dev/null +++ b/tests/libs/action.json @@ -0,0 +1,14 @@ +{ + "保存": { + "type": "button", + "props": { + "label": "保存" + } + }, + "删除": { + "type": "button", + "props": { + "label": "保存" + } + } +} diff --git a/tests/libs/apis/table.json b/tests/libs/apis/table.json new file mode 100644 index 00000000..3cf780c3 --- /dev/null +++ b/tests/libs/apis/table.json @@ -0,0 +1,5 @@ +{ + "Search": { + "default": [null, null, "{{$in.0}}"] + } +} diff --git a/tests/libs/column.json b/tests/libs/column.json new file mode 100644 index 00000000..69426bbb --- /dev/null +++ b/tests/libs/column.json @@ -0,0 +1,32 @@ +{ + "Label": { + "__comment": "column.Lable('ID',':id')", + "label": "{{$in.0}}", + "view": { + "props": { + "value": "{{$in.1}}" + } + } + }, + "Image": { + "__comment": "column.Image('图标',':icon', 40)", + "label": "{{$in.0}}", + "view": { + "type": "image", + "props": { + "value": "{{$in.1}}", + "width": "{{$in.2}}", + "height": "{{$in.2}}" + } + }, + "edit": { + "type": "upload", + "props": { + "value": "{{$in.1}}", + "filetype": "image", + "multiple": false, + "maxCount": 1 + } + } + } +} diff --git a/tests/libs/filter.json b/tests/libs/filter.json new file mode 100644 index 00000000..f8717d55 --- /dev/null +++ b/tests/libs/filter.json @@ -0,0 +1,47 @@ +{ + "关键词": { + "__comment": "filter.关键词('where.name.match')", + "label": "关键词", + "bind": "{{$in.0}}", + "input": { + "type": "input", + "props": { + "placeholder": "请输入关键词" + } + } + }, + "排序": { + "__comment": "filter.排序()", + "label": "排序方式", + "bind": "order", + "input": { + "type": "select", + "props": { + "placeholder": "排序方式", + "options": [ + { "label": "手动顺序", "value": "rank" }, + { "label": "最近更新", "value": "updated_at.desc" }, + { "label": "最近创建", "value": "created_at.desc" } + ] + } + } + }, + "客户状态": { + "__comment": "filter.客户状态()", + "label": "状态", + "bind": "where.status.in", + "input": { + "type": "select", + "props": { + "placeholder": "选择厂商状态", + "mode": "multiple", + "options": [ + { "value": "已注册" }, + { "value": "待收录" }, + { "value": "待审核" }, + { "value": "已禁用" } + ] + } + } + } +} diff --git a/tests/libs/pages/static.json b/tests/libs/pages/static.json new file mode 100644 index 00000000..65092d2e --- /dev/null +++ b/tests/libs/pages/static.json @@ -0,0 +1,16 @@ +{ + "Page": { + "primary": "{{$in.0}}", + "layout": { + "filters": [ + { "name": "开始时间", "width": 6 }, + { "name": "结束时间", "width": 6 } + ], + "charts": [ + { "type": "line", "props": {} }, + { "type": "bar", "props": {} } + ] + }, + "actions": {} + } +} diff --git a/tests/libs/render.json b/tests/libs/render.json new file mode 100644 index 00000000..f2261d0e --- /dev/null +++ b/tests/libs/render.json @@ -0,0 +1,10 @@ +{ + "Image": { + "type": "image", + "props": { + "value": "{{$in.0}}", + "width": "{{$in.1}}", + "height": "{{$in.2}}" + } + } +} diff --git a/tests/models/customer.mod.json b/tests/models/customer.mod.json new file mode 100644 index 00000000..8ccca686 --- /dev/null +++ b/tests/models/customer.mod.json @@ -0,0 +1,230 @@ +{ + "name": "客户", + "table": { + "name": "customer", + "comment": "客户表", + "engine": "InnoDB" + }, + "columns": [ + { + "label": "ID", + "name": "id", + "type": "ID", + "comment": "ID" + }, + { + "label": "来源", + "name": "channel_id", + "type": "bigInteger", + "comment": "客户来源" + }, + { + "label": "公司名称", + "name": "name", + "type": "string", + "length": 200, + "comment": "公司全称", + "unique": true + }, + { + "label": "公司简称", + "name": "short_name", + "type": "string", + "length": 200, + "comment": "简称", + "nullable": true, + "index": true + }, + { + "name": "credit_no", + "type": "string", + "length": 200, + "comment": "统一社会信用代码", + "nullable": true, + "unique": true + }, + { + "name": "oper_name", + "type": "string", + "length": 50, + "comment": "法人代表", + "nullable": true, + "index": true + }, + { + "label": "注册资本(万元)", + "name": "reg_capi", + "type": "integer", + "comment": "注册资本(万元)", + "nullable": true, + "index": true + }, + { + "label": "注册时间", + "name": "opened_at", + "type": "date", + "comment": "注册时间", + "nullable": true, + "index": true + }, + { + "label": "省份", + "name": "province", + "type": "string", + "length": 50, + "comment": "总部所在省份", + "nullable": true, + "index": true + }, + { + "label": "城市", + "name": "city", + "type": "string", + "length": 100, + "comment": "总部所在城市", + "nullable": true, + "index": true + }, + { + "label": "地址", + "name": "address", + "type": "string", + "length": 255, + "comment": "总部地址", + "nullable": true, + "index": true + }, + { + "label": "经度", + "name": "lng", + "type": "unsignedDecimal", + "precision": 12, + "scale": 6, + "comment": "经度", + "nullable": true, + "index": true + }, + { + "label": "纬度", + "name": "lat", + "type": "unsignedDecimal", + "comment": "纬度", + "precision": 12, + "scale": 6, + "nullable": true, + "index": true + }, + { + "label": "经营状态", + "name": "company_status", + "type": "string", + "length": 255, + "comment": "经营状态", + "nullable": true, + "index": true + }, + { + "label": "LOGO", + "name": "logo", + "type": "json", + "comment": "LOGO", + "nullable": true + }, + { + "label": "简介", + "name": "summary", + "type": "string", + "length": 600, + "comment": "简介", + "nullable": true, + "index": true + }, + { + "label": "官网", + "name": "link", + "type": "string", + "length": 200, + "comment": "官网地址", + "nullable": true + }, + { + "label": "联系人", + "name": "contact_name", + "type": "string", + "length": 200, + "comment": "联系人姓名", + "nullable": true, + "index": true + }, + { + "label": "联系人职务", + "name": "contact_title", + "type": "string", + "length": 200, + "comment": "联系人职务", + "nullable": true, + "index": true + }, + { + "label": "联系电话", + "name": "contact_mobile", + "type": "string", + "comment": "联系电话", + "nullable": true, + "index": true + }, + { + "label": "联系微信", + "name": "contact_wechat", + "type": "string", + "comment": "联系微信", + "nullable": true, + "index": true + }, + { + "label": "介绍", + "name": "desc", + "type": "text", + "comment": "介绍", + "nullable": true + }, + { + "label": "领域", + "name": "fields", + "type": "json", + "comment": "服务领域(多选)", + "nullable": true, + "index": true + }, + { + "label": "行业", + "name": "industries", + "type": "json", + "comment": "行业覆盖(多选)", + "nullable": true, + "index": true + }, + { + "label": "标签", + "name": "tags", + "type": "json", + "comment": "标签(多选)", + "nullable": true, + "index": true + }, + { + "label": "关系", + "name": "relation", + "type": "enum", + "default": "正在接洽", + "option": ["已有合作", "正在接洽", "潜在客户"], + "comment": "合作关系: 已有合作, 正在接洽, 潜在客户", + "index": true + } + ], + "relations": {}, + "option": { + "timestamps": true, + "soft_deletes": true + } +} diff --git a/tests/models/service.json b/tests/models/service.json index f624af76..61a71f74 100644 --- a/tests/models/service.json +++ b/tests/models/service.json @@ -27,6 +27,7 @@ "type": "string", "length": 50, "comment": "城市", + "nullable": true, "index": true }, { diff --git a/tests/tables/importable/table.tab.json b/tests/tables/importable/table.tab.json new file mode 100644 index 00000000..d5294dd4 --- /dev/null +++ b/tests/tables/importable/table.tab.json @@ -0,0 +1,75 @@ +{ + "name": "客户", + "version": "1.0.0", + "decription": "客户管理数据表格", + "bind": { + "model": "user" + }, + "apis": { + "search": {} + }, + "columns": { + "LOGO": { "@": "column.Image", "in": ["LOGO", ":logo", 40] }, + "ID": { "@": "column.Label", "in": ["ID", ":id"] } + }, + "filters": { + "关键词": { "@": "filter.关键词", "in": ["where.name.match"] }, + "排序": { "@": "filter.排序" }, + "状态": { "@": "filter.客户状态" } + }, + "list": { + "primary": "id", + "layout": { + "columns": [ + { "name": "ID", "width": 80 }, + { "name": "LOGO", "width": 300 } + ], + "filters": [ + { "name": "关键词" }, + { "name": "状态", "width": 3 }, + { "name": "排序", "width": 3 } + ] + }, + "actions": { + "create": { + "type": "button", + "props": { + "label": "添加厂商", + "icon": "fas fa-plus" + } + }, + "pagination": { + "props": { "showTotal": true } + } + }, + "option": { + "batch": { + "delete": {}, + "columns": [ + { "name": "状态", "width": 12 }, + { "name": "类型", "width": 12 }, + { "name": "排序", "width": 12 } + ] + } + } + }, + "edit": { + "primary": "id", + "layout": { + "fieldset": [ + { + "title": "客户资料", + "description": "", + "columns": [{ "name": "LOGO", "width": 12 }] + } + ] + }, + "actions": { + "cancel": {}, + "save": { "@": "action.保存" }, + "delete": { "@": "action.删除" } + } + }, + "insert": {}, + "view": {} +}