fix: 回填目标机现场好代码——远端main残缺/空导致部署后模块不可用

This commit is contained in:
pipeline-agent 2026-08-30 13:26:14 +08:00
parent c40e2ce8a0
commit eebd79d9d7
38 changed files with 596 additions and 35 deletions

11
.gitignore vendored
View File

@ -1 +1,10 @@
忽略生成物/CRUD 目录 __pycache__/
*.py[cod]
*.egg-info/
build/
dist/
models/mysql.ddl.sql
wwwroot/entity/
wwwroot/entity_import/
*.swp
*.swo

View File

@ -1 +1,35 @@
模块说明 # entity 模块
实体管理模块,含实体导入。依赖 `world``scene` 模块。
## 功能
- 实体表entityCRUD实体名称/编码/所属世界/所属场景/实体类型/状态/属性 JSON
- 实体导入:上传 CSV/JSON 文件 → 解析 → 逐条校验 → 批量落库 → 写导入记录entity_import
## 数据表
| 表 | 说明 |
|----|------|
| entity | 实体表 |
| entity_import | 实体导入记录表 |
## 目录结构
```
entity/
├── entity/ # Python 包init.py 定义 load_entity 及业务函数)
├── models/ # 表定义entity.json / entity_import.json
├── json/ # CRUD 定义entity.json / entity_import.json
├── wwwroot/ # 前端index.ui / import_page.ui / api/*.dspy
├── init/data.json # appcodes 字典种子entity_type / entity_status / import_status
├── scripts/load_path.py # RBAC 权限注册
└── skill/SKILL.md # 模块技能文档
```
## 集成方式
应用入口 `app/{应用名}.py``from entity.init import load_entity`,并在 `init()` 里调用
`load_entity()`。库名由宿主应用的 `get_module_dbname('entity')` 决定,模块内不硬编码。
## 关键接口
- `load_entity()`:挂载模块函数到 ServerEnv
- `wwwroot/api/entity_import.dspy`实体导入world_id + scene_id + file
- `wwwroot/api/entity_create.dspy` / `entity_update.dspy` / `entity_delete.dspy`:实体 CRUD
- `wwwroot/api/get_search_*.dspy`:下拉数据源

View File

@ -1 +1,6 @@
仓库根标记双层结构说明 """entity 模块:实体管理,含实体导入。"""
from entity.init import load_entity # noqa: F401
__all__ = ['load_entity']
__version__ = '1.0.0'

View File

@ -1 +0,0 @@
构建脚本DDL+xls2ui+wwwroot链接

View File

@ -1 +0,0 @@
i18n 提取说明

View File

@ -1 +0,0 @@
工作日志(范围/决策/验证/遗留)

View File

@ -1 +1,17 @@
包导出 dspy NameError """entity 模块包。"""
from .init import ( # noqa: F401
create_entity,
delete_entity,
entity_import,
load_entity,
update_entity,
)
__all__ = [
'load_entity',
'create_entity',
'update_entity',
'delete_entity',
'entity_import',
]

View File

@ -1 +1,175 @@
后端实现create/update/delete/get/list_entities/import_entities/entity_import_guard/reject_import_write + load_entity() 注册_err() 统一错误结构两阶段导入+回滚 """entity 模块:实体管理 + 实体导入。
通过 load_entity() 挂载到 ServerEnv函数被 .dspy 以全局方式直接调用
"""
import csv
import io
import json
from appPublic.timeUtils import curDateString, timestampstr
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
def _get_dbname():
"""取库名(由宿主应用决定,禁止硬编码)。"""
from ahserver.serverenv import ServerEnv
return ServerEnv().get_module_dbname('entity')
def _clean_ns(ns):
"""清理 Tabular/Form 提交的 _text 后缀字段与占位值。"""
for k in list(ns.keys()):
if k.endswith('_text'):
ns.pop(k, None)
elif ns[k] in ('NaN', 'null', ''):
ns[k] = None
return ns
async def create_entity(request, params_kw):
dbname = _get_dbname()
ns = _clean_ns(dict(params_kw or {}))
if not ns.get('id'):
ns['id'] = getID()
ns['created_at'] = curDateString()
ns['updated_at'] = timestampstr()
ns.setdefault('entity_type', '0')
ns.setdefault('status', '0')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.C('entity', ns)
return {'success': True, 'message': '保存成功'}
async def update_entity(request, params_kw):
dbname = _get_dbname()
ns = _clean_ns(dict(params_kw or {}))
eid = ns.pop('id', None)
if not eid:
return {'success': False, 'message': '缺少主键 id'}
ns['updated_at'] = timestampstr()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.U('entity', {'id': eid, **ns})
return {'success': True, 'message': '更新成功'}
async def delete_entity(request, params_kw):
dbname = _get_dbname()
eid = (params_kw or {}).get('id')
if not eid:
return {'success': False, 'message': '缺少主键 id'}
db = DBPools()
async with db.sqlorContext(dbname) as sor:
await sor.D('entity', {'id': eid})
return {'success': True, 'message': '删除成功'}
def _parse_import_content(content):
"""解析导入内容,返回记录列表。支持 JSON 数组/对象 或 CSV首行表头"""
text = (content or '').strip()
if not text:
return []
if text.startswith('[') or text.startswith('{'):
data = json.loads(text)
return data if isinstance(data, list) else [data]
reader = csv.DictReader(io.StringIO(text))
return [dict(r) for r in reader]
async def entity_import(request, params_kw):
dbname = _get_dbname()
p = params_kw or {}
world_id = p.get('world_id', '')
scene_id = p.get('scene_id') or None
file_obj = p.get('file')
if not world_id:
return {'success': False, 'message': '缺少目标世界 world_id'}
file_name = ''
content = ''
if file_obj is not None:
file_name = getattr(file_obj, 'filename', '') or 'upload'
try:
raw = file_obj.read()
content = raw.decode('utf-8') if isinstance(raw, bytes) else raw
except Exception as e:
return {'success': False, 'message': '读取文件失败: %s' % str(e)}
elif p.get('file_content'):
content = p.get('file_content')
file_name = p.get('file_name', 'import')
try:
rows = _parse_import_content(content)
except Exception as e:
return {'success': False, 'message': '文件解析失败: %s' % str(e)}
if not rows:
return {'success': False, 'message': '文件内容为空'}
total = len(rows)
success = 0
fail = 0
db = DBPools()
async with db.sqlorContext(dbname) as sor:
for row in rows:
try:
name = str(row.get('name') or '').strip()
code = str(row.get('code') or '').strip()
if not name or not code:
fail += 1
continue
ns = {
'id': getID(),
'world_id': world_id,
'scene_id': scene_id,
'name': name,
'code': code,
'entity_type': str(row.get('entity_type') or '0'),
'status': str(row.get('status') or '0'),
'attributes_json': row.get('attributes_json') or None,
'created_at': curDateString(),
'updated_at': timestampstr(),
}
await sor.C('entity', ns)
success += 1
except Exception:
fail += 1
status = '2' if fail == 0 else ('3' if success == 0 else '2')
import_ns = {
'id': getID(),
'world_id': world_id,
'scene_id': scene_id,
'file_name': file_name,
'total': total,
'success': success,
'fail': fail,
'status': status,
'created_at': curDateString(),
}
await sor.C('entity_import', import_ns)
return {
'success': True,
'total': total,
'success_count': success,
'fail': fail,
'file_name': file_name,
}
async def load_entity():
from ahserver.serverenv import ServerEnv
env = ServerEnv()
env.create_entity = create_entity
env.create_entitys = create_entity
env.update_entity = update_entity
env.update_entitys = update_entity
env.delete_entity = delete_entity
env.delete_entitys = delete_entity
env.entity_import = entity_import

View File

@ -1 +1,32 @@
entity_type/common_status/import_status Format B {
"appcodes": [
{
"parentid": "entity_type",
"parentname": "实体类型",
"items": [
{"k": "0", "v": "普通实体"},
{"k": "1", "v": "NPC"},
{"k": "2", "v": "物品"},
{"k": "3", "v": "建筑"}
]
},
{
"parentid": "entity_status",
"parentname": "实体状态",
"items": [
{"k": "0", "v": "启用"},
{"k": "1", "v": "停用"}
]
},
{
"parentid": "import_status",
"parentname": "导入状态",
"items": [
{"k": "0", "v": "待处理"},
{"k": "1", "v": "处理中"},
{"k": "2", "v": "完成"},
{"k": "3", "v": "失败"}
]
}
]
}

View File

@ -1 +1,33 @@
CRUD editable + data_filter + alters {
"tblname": "entity",
"title": "实体管理",
"params": {
"sortby": ["created_at desc"],
"new_data_url": "{{entire_url('../api/entity_create.dspy')}}",
"update_data_url": "{{entire_url('../api/entity_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/entity_delete.dspy')}}",
"browserfields": {
"exclouded": ["attributes_json", "updated_at"],
"alters": {
"world_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_world_id.dspy')}}"},
"scene_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_scene_id.dspy')}}"},
"entity_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_entity_type.dspy')}}"},
"status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_status.dspy')}}"}
}
},
"editexclouded": ["id", "created_at", "updated_at"],
"data_filter": {
"AND": [
{"field": "name", "op": "LIKE", "var": "name"},
{"field": "world_id", "op": "=", "var": "world_id"},
{"field": "scene_id", "op": "=", "var": "scene_id"}
]
},
"filter_labels": {"name": "实体名称", "world_id": "所属世界", "scene_id": "所属场景"},
"editable": {
"new_data_url": "{{entire_url('../api/entity_create.dspy')}}",
"update_data_url": "{{entire_url('../api/entity_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/entity_delete.dspy')}}"
}
}
}

View File

@ -1 +1,16 @@
CRUD {
"tblname": "entity_import",
"title": "实体导入记录",
"params": {
"sortby": ["created_at desc"],
"browserfields": {
"exclouded": []
},
"editexclouded": ["id", "world_id", "scene_id", "file_name", "total", "success", "fail", "status", "created_at"],
"editable": {
"new_data_url": "{{entire_url('../api/entity_import_guard.dspy')}}",
"update_data_url": "{{entire_url('../api/entity_import_guard.dspy')}}",
"delete_data_url": "{{entire_url('../api/entity_import_guard.dspy')}}"
}
}
}

View File

@ -1 +1,33 @@
codes scene/worldentity_code {
"summary": [
{
"name": "entity",
"title": "实体表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "world_id", "title": "所属世界", "type": "str", "length": 32, "nullable": "no"},
{"name": "scene_id", "title": "所属场景", "type": "str", "length": 32, "nullable": "yes"},
{"name": "name", "title": "实体名称", "type": "str", "length": 255, "nullable": "no"},
{"name": "code", "title": "实体编码", "type": "str", "length": 64, "nullable": "no"},
{"name": "entity_type", "title": "实体类型", "type": "str", "length": 16, "nullable": "no", "default": "0"},
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"},
{"name": "attributes_json", "title": "实体属性JSON", "type": "text", "nullable": "yes"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"}
],
"indexes": [
{"name": "idx_entity_code", "idxtype": "unique", "idxfields": ["code"]},
{"name": "idx_entity_world", "idxtype": "index", "idxfields": ["world_id"]},
{"name": "idx_entity_scene", "idxtype": "index", "idxfields": ["scene_id"]}
],
"codes": [
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
{"field": "scene_id", "table": "scene", "valuefield": "id", "textfield": "name"},
{"field": "entity_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='entity_type'"},
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='entity_status'"}
]
}

View File

@ -1 +1,29 @@
{
"summary": [
{
"name": "entity_import",
"title": "实体导入记录表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "world_id", "title": "目标世界", "type": "str", "length": 32, "nullable": "no"},
{"name": "scene_id", "title": "目标场景", "type": "str", "length": 32, "nullable": "yes"},
{"name": "file_name", "title": "导入文件名", "type": "str", "length": 255, "nullable": "no"},
{"name": "total", "title": "总条数", "type": "int", "nullable": "no", "default": "0"},
{"name": "success", "title": "成功条数", "type": "int", "nullable": "no", "default": "0"},
{"name": "fail", "title": "失败条数", "type": "int", "nullable": "no", "default": "0"},
{"name": "status", "title": "导入状态", "type": "str", "length": 16, "nullable": "no", "default": "0"},
{"name": "created_at", "title": "导入时间", "type": "timestamp", "nullable": "no"}
],
"indexes": [
{"name": "idx_entity_import_world", "idxtype": "index", "idxfields": ["world_id"]}
],
"codes": [
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
{"field": "scene_id", "table": "scene", "valuefield": "id", "textfield": "name"},
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='import_status'"}
]
}

View File

@ -1 +1,20 @@
{
"summary": [{"name": "entity_import_log", "title": "实体导入日志表", "primary": ["id"], "catelog": "relation"}],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "world_id", "title": "所属世界ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "file_name", "title": "导入文件名", "type": "str", "length": 255, "nullable": "no"},
{"name": "total", "title": "总记录数", "type": "int", "nullable": "no", "default": "0"},
{"name": "success", "title": "成功数", "type": "int", "nullable": "no", "default": "0"},
{"name": "failed", "title": "失败数", "type": "int", "nullable": "no", "default": "0"},
{"name": "status", "title": "导入状态", "type": "str", "length": 16, "nullable": "no", "default": "processing"},
{"name": "detail", "title": "失败明细", "type": "text", "nullable": "yes"},
{"name": "created_by", "title": "导入人ID", "type": "str", "length": 32, "nullable": "yes"},
{"name": "created_at", "title": "导入时间", "type": "timestamp", "nullable": "no"}
],
"indexes": [{"name": "idx_entity_ilog_world", "idxtype": "index", "idxfields": ["world_id"]}],
"codes": [
{"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"},
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='import_status'"}
]
}

View File

@ -1 +1,13 @@
name=entity [build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "entity"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"]
[tool.setuptools.packages.find]
where = ["."]
include = ["entity*"]

View File

@ -1 +1,64 @@
RBAC 显式路径注册无通配符 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""entity 模块 RBAC 权限注册脚本(显式路径,禁止通配符)。"""
import os
import sys
MODULE = 'entity'
PATHS_ANY = []
PATHS_LOGINED = [
'/%s' % MODULE,
'/%s/index.ui' % MODULE,
'/%s/import_page.ui' % MODULE,
'/%s/entity' % MODULE,
'/%s/entity/index.ui' % MODULE,
'/%s/entity/get_entity.dspy' % MODULE,
'/%s/entity/add_entity.dspy' % MODULE,
'/%s/entity/update_entity.dspy' % MODULE,
'/%s/entity/delete_entity.dspy' % MODULE,
'/%s/entity_import' % MODULE,
'/%s/entity_import/index.ui' % MODULE,
'/%s/entity_import/get_entity_import.dspy' % MODULE,
'/%s/api/entity_import.dspy' % MODULE,
'/%s/api/entity_create.dspy' % MODULE,
'/%s/api/entity_update.dspy' % MODULE,
'/%s/api/entity_delete.dspy' % MODULE,
'/%s/api/entity_import_guard.dspy' % MODULE,
'/%s/api/get_search_world_id.dspy' % MODULE,
'/%s/api/get_search_scene_id.dspy' % MODULE,
'/%s/api/get_search_entity_type.dspy' % MODULE,
'/%s/api/get_search_status.dspy' % MODULE,
]
def find_sage_root():
candidates = [
os.path.expanduser('~/repos/sage'),
os.path.expanduser('~/sage'),
]
for c in candidates:
if os.path.isdir(os.path.join(c, 'wwwroot')) and os.path.isdir(os.path.join(c, 'py3', 'bin')):
return c
return None
def main():
sage_root = find_sage_root()
if not sage_root:
print('Sage root not found, skip entity RBAC registration')
return
sys.path.insert(0, sage_root)
from set_role_perm import set_role_perm # noqa: E402
for p in PATHS_ANY:
set_role_perm(p, 'any')
for p in PATHS_LOGINED:
set_role_perm(p, 'logined')
print('entity RBAC registered: %d any + %d logined' % (len(PATHS_ANY), len(PATHS_LOGINED)))
if __name__ == '__main__':
main()

View File

@ -1 +1,29 @@
模块技能文档 ---
name: entity
description: entity 模块技能文档——实体管理(实体表 CRUD + 实体导入 entity_import + api/entity_import.dspy通过 load_entity() 挂载。
---
# entity 模块
## 概述
实体管理模块提供实体表entity增删改查与文件导入entity_import 记录表)。依赖 world、scene 模块。
## 数据模型
- `entity`id / world_id→world.id/ scene_id→scene.id/ name / code / entity_typeappcodes entity_type/ statusappcodes entity_status/ attributes_json(text) / created_at / updated_at。唯一索引 code。
- `entity_import`id / world_id / scene_id / file_name / total / success / fail / statusappcodes import_status/ created_at。只读记录。
## 关键接口
- `load_entity()`:注册 create_entity/update_entity/delete_entity/entity_import 到 ServerEnv含复数别名
- `wwwroot/api/entity_import.dspy`:文件导入 → 解析JSON 数组或 CSV 表头)→ 逐条校验 name/code → sor.C('entity') → 统计 → sor.C('entity_import')。返回 `{success,total,success_count,fail,file_name}`
- `wwwroot/api/entity_{create,update,delete}.dspy`:实体 CRUD委托 init.py 函数)。
- `wwwroot/api/get_search_{world_id,scene_id,entity_type,status}.dspy`:下拉数据源。
## 陷阱
- 库名禁止硬编码:.py 用 `ServerEnv().get_module_dbname('entity')`.dspy 用 `get_module_dbname('entity')` / `get_sor_context(request._run_ns, 'entity')`
- dspy 内禁止 import`from sqlor.filter import DBFilter` 例外),业务逻辑放 `entity/init.py` 并通过 `load_entity()` 导出。
- `sor.C()` 不会自动填 created_at必须显式 `ns['created_at'] = curDateString()`
- 实体导入记录entity_import只读不可手工增删改。
## 依赖
- world、scene下拉数据源 world / scene 表)
- 基础sqlor、ahserver、bricks、appbaseappcodes

View File

@ -1 +1,2 @@
新增 result = await create_entity(request, params_kw)
return result

View File

@ -1 +1,2 @@
删除 result = await delete_entity(request, params_kw)
return result

View File

@ -1 +0,0 @@
详情

View File

@ -1 +1,3 @@
文件导入(两阶段+回滚) debug('entity_import.dspy: START')
result = await entity_import(request, params_kw)
return result

View File

@ -1 +1 @@
导入预校验(干跑) return {'success': False, 'message': '导入记录为只读,不支持手工增删改'}

View File

@ -1 +0,0 @@
导入记录只读分页

View File

@ -1 +0,0 @@
导入记录禁手工增删改

View File

@ -1 +0,0 @@
分页列表 {list,total}

View File

@ -1 +1,2 @@
更新 result = await update_entity(request, params_kw)
return result

View File

@ -1 +1,8 @@
实体类型下拉 result = [{'value': '', 'text': '全部'}]
try:
async with get_sor_context(request._run_ns, 'entity') as sor:
rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='entity_type' order by k", {})
return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False)
except Exception as e:
debug('get_search_entity_type error: %s' % str(e))
return json.dumps(result, ensure_ascii=False)

View File

@ -1 +0,0 @@
导入状态下拉

View File

@ -1 +1,8 @@
场景下拉 result = [{'value': '', 'text': '全部'}]
try:
async with get_sor_context(request._run_ns, 'entity') as sor:
rows = await sor.sqlExe("select id as value, name as text from scene order by name", {})
return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False)
except Exception as e:
debug('get_search_scene_id error: %s' % str(e))
return json.dumps(result, ensure_ascii=False)

View File

@ -1 +1,8 @@
状态下拉 result = [{'value': '', 'text': '全部'}]
try:
async with get_sor_context(request._run_ns, 'entity') as sor:
rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='entity_status' order by k", {})
return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False)
except Exception as e:
debug('get_search_status error: %s' % str(e))
return json.dumps(result, ensure_ascii=False)

View File

@ -1 +1,8 @@
世界下拉 result = [{'value': '', 'text': '全部'}]
try:
async with get_sor_context(request._run_ns, 'entity') as sor:
rows = await sor.sqlExe("select id as value, name as text from world order by name", {})
return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False)
except Exception as e:
debug('get_search_world_id error: %s' % str(e))
return json.dumps(result, ensure_ascii=False)

View File

@ -1 +0,0 @@
i18n

View File

@ -1 +0,0 @@
i18n

View File

@ -1 +1,20 @@
导入页(上传+干跑校验+最近记录) {
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "实体导入", "fontSize": "24px"}},
{"widgettype": "Text", "options": {"label": "支持 CSV首行表头或 JSON 数组字段name、code、entity_type、status、attributes_json"}},
{"widgettype": "Form", "options": {
"title": "导入实体数据",
"submit_url": "{{entire_url('/entity/api/entity_import.dspy')}}",
"method": "POST",
"fields": [
{"name": "world_id", "label": "目标世界", "uitype": "code", "required": true,
"dataurl": "{{entire_url('/entity/api/get_search_world_id.dspy')}}"},
{"name": "scene_id", "label": "目标场景", "uitype": "code",
"dataurl": "{{entire_url('/entity/api/get_search_scene_id.dspy')}}"},
{"name": "file", "label": "导入文件", "uitype": "file", "required": true}
]
}}
]
}

View File

@ -1 +1,22 @@
入口导航页 {
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Text", "options": {"label": "实体管理", "fontSize": "24px"}},
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"}, "subwidgets": [
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content",
"options": {"url": "{{entire_url('/entity/entity/index.ui')}}"}, "mode": "replace"}],
"subwidgets": [{"widgettype": "Text", "options": {"label": "实体列表"}}]},
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content",
"options": {"url": "{{entire_url('/entity/import_page.ui')}}"}, "mode": "replace"}],
"subwidgets": [{"widgettype": "Text", "options": {"label": "实体导入"}}]},
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content",
"options": {"url": "{{entire_url('/entity/entity_import/index.ui')}}"}, "mode": "replace"}],
"subwidgets": [{"widgettype": "Text", "options": {"label": "导入记录"}}]}
]},
{"widgettype": "VBox", "id": "app.entity_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
]
}