1625 lines
64 KiB
Python
1625 lines
64 KiB
Python
"""
|
||
JeecgBoot 设计器表单(desform)通用工具库
|
||
==========================================
|
||
提供 API 调用、控件工厂、表单组装、菜单SQL生成等共通功能。
|
||
|
||
使用示例:
|
||
from desform_utils import *
|
||
init_api('https://boot3.jeecg.com/jeecgboot', 'your-token')
|
||
form_id, uc = find_or_create_form('Customer Info', 'customer_info')
|
||
widgets = [
|
||
INPUT('客户名称', required=True),
|
||
PHONE('电话'),
|
||
SELECT('类型', options=['企业', '个人']),
|
||
]
|
||
save_design(form_id, 'customer_info', widgets, title_index=0, update_count=uc)
|
||
"""
|
||
|
||
import urllib.request
|
||
import json
|
||
import time
|
||
import random
|
||
import ssl
|
||
import uuid
|
||
|
||
# ============================================================
|
||
# 全局配置
|
||
# ============================================================
|
||
_API_BASE = ''
|
||
_TOKEN = ''
|
||
_SSL_CTX = ssl.create_default_context()
|
||
_SSL_CTX.check_hostname = False
|
||
_SSL_CTX.verify_mode = ssl.CERT_NONE
|
||
|
||
# 固定角色ID(用于授权SQL)
|
||
ROLE_ID = 'f6817f48af4fb3af11b9e8bf182f618b'
|
||
|
||
# 表单缓存: {code: {'id': str, 'uc': int}}
|
||
_FORM_CACHE = {}
|
||
|
||
|
||
def clear_cache():
|
||
"""清空 Python 内存缓存"""
|
||
global _FORM_CACHE
|
||
_FORM_CACHE = {}
|
||
|
||
|
||
def init_api(api_base, token):
|
||
"""初始化 API 地址和 Token"""
|
||
global _API_BASE, _TOKEN
|
||
_API_BASE = api_base.rstrip('/')
|
||
_TOKEN = token
|
||
|
||
|
||
# ============================================================
|
||
# API 请求
|
||
# ============================================================
|
||
def api_request(path, data=None, method='POST'):
|
||
"""发送 API 请求,返回 JSON 响应"""
|
||
url = f'{_API_BASE}{path}'
|
||
headers = {
|
||
'X-Access-Token': _TOKEN,
|
||
'X-Sign': '00000000000000000000000000000000',
|
||
'X-Tenant-Id': '1',
|
||
'X-Timestamp': str(int(time.time() * 1000)),
|
||
'Content-Type': 'application/json; charset=UTF-8'
|
||
}
|
||
if data is not None:
|
||
json_data = json.dumps(data, ensure_ascii=False).encode('utf-8')
|
||
req = urllib.request.Request(url, data=json_data, headers=headers, method=method)
|
||
else:
|
||
req = urllib.request.Request(url, headers=headers, method=method)
|
||
resp = urllib.request.urlopen(req, context=_SSL_CTX)
|
||
return json.loads(resp.read().decode('utf-8'))
|
||
|
||
|
||
# ============================================================
|
||
# 字典查询
|
||
# ============================================================
|
||
def query_dict(dict_code):
|
||
"""查询字典项列表,返回 [{value, text, label, ...}, ...]
|
||
|
||
用法: query_dict('sex') → [{'value': '1', 'text': '男'}, {'value': '2', 'text': '女'}]
|
||
"""
|
||
r = api_request(f'/sys/dict/getDictItems/{dict_code}', method='GET')
|
||
if r.get('success') and r.get('result'):
|
||
return r['result']
|
||
return []
|
||
|
||
|
||
def search_dict(keyword):
|
||
"""通过关键词模糊搜索字典编码,返回匹配的字典列表 [{id, dictCode, dictName, ...}, ...]
|
||
|
||
用法: search_dict('性别') → [{'dictCode': 'sex', 'dictName': '性别', ...}]
|
||
search_dict('sex') → [{'dictCode': 'sex', 'dictName': '性别', ...}]
|
||
"""
|
||
r = api_request(f'/sys/dict/list?pageNo=1&pageSize=200&dictName={keyword}', method='GET')
|
||
results = []
|
||
if r.get('success') and r.get('result'):
|
||
records = r['result'].get('records', [])
|
||
results.extend(records)
|
||
# 也按 dictCode 搜索
|
||
r2 = api_request(f'/sys/dict/list?pageNo=1&pageSize=200&dictCode={keyword}', method='GET')
|
||
if r2.get('success') and r2.get('result'):
|
||
seen_ids = {rec['id'] for rec in results}
|
||
for rec in r2['result'].get('records', []):
|
||
if rec['id'] not in seen_ids:
|
||
results.append(rec)
|
||
return results
|
||
|
||
|
||
# ============================================================
|
||
# 表单缓存 & 查找
|
||
# ============================================================
|
||
def _cache_put(code, form_id, uc=0):
|
||
"""写入缓存"""
|
||
_FORM_CACHE[code] = {'id': form_id, 'uc': uc}
|
||
|
||
|
||
def _cache_get(code):
|
||
"""读取缓存,返回 (id, uc) 或 (None, None)"""
|
||
c = _FORM_CACHE.get(code)
|
||
if c:
|
||
return c['id'], c['uc']
|
||
return None, None
|
||
|
||
|
||
def _cache_remove(code):
|
||
"""清除缓存"""
|
||
_FORM_CACHE.pop(code, None)
|
||
|
||
|
||
def _find_by_list(code):
|
||
"""通过 list API 全量搜索 + 精确匹配 desformCode 查找表单(按创建时间倒序,取最新的)"""
|
||
page = 1
|
||
while page <= 10:
|
||
r = api_request(f'/desform/list?pageNo={page}&pageSize=100&column=createTime&order=desc', method='GET')
|
||
if not r.get('success') or not r.get('result'):
|
||
break
|
||
records = r['result'].get('records', [])
|
||
if not records:
|
||
break
|
||
for rec in records:
|
||
if rec.get('desformCode') == code:
|
||
fid, uc = rec['id'], rec.get('updateCount', 0)
|
||
_cache_put(code, fid, uc)
|
||
return fid, uc
|
||
total = r['result'].get('total', 0)
|
||
if page * 100 >= total:
|
||
break
|
||
page += 1
|
||
return None, None
|
||
|
||
|
||
def _verify_form_exists(form_id):
|
||
"""验证表单 ID 是否真实存在(通过 list API 验证,不走缓存)"""
|
||
try:
|
||
# list API 不走 Redis 缓存,结果可靠
|
||
page = 1
|
||
while page <= 5:
|
||
r = api_request(f'/desform/list?pageNo={page}&pageSize=100&column=createTime&order=desc', method='GET')
|
||
if not r.get('success') or not r.get('result'):
|
||
return False
|
||
for rec in r['result'].get('records', []):
|
||
if rec.get('id') == form_id:
|
||
return True
|
||
total = r['result'].get('total', 0)
|
||
if page * 100 >= total:
|
||
break
|
||
page += 1
|
||
return False
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def get_form_id(code):
|
||
"""通过表单编码获取表单 ID(带缓存),返回 (form_id, update_count) 或 (None, None)
|
||
|
||
查找顺序: 缓存 → queryByCode(带验证) → list 全量搜索
|
||
"""
|
||
# 1. 缓存(已验证过的)
|
||
fid, uc = _cache_get(code)
|
||
if fid:
|
||
return fid, uc
|
||
|
||
# 2. queryByCode(需要验证,该接口有服务端缓存可能返回已删除的幽灵记录)
|
||
try:
|
||
r = api_request(f'/desform/queryByCode?desformCode={code}', method='GET')
|
||
if r.get('success') and r.get('result') and r['result'].get('id'):
|
||
fid = r['result']['id']
|
||
uc = r['result'].get('updateCount', 0)
|
||
# 验证 ID 是否真实存在
|
||
if _verify_form_exists(fid):
|
||
_cache_put(code, fid, uc)
|
||
return fid, uc
|
||
# 幽灵记录,跳过
|
||
except Exception:
|
||
pass
|
||
|
||
# 3. list 全量搜索(list 结果比较可靠)
|
||
return _find_by_list(code)
|
||
|
||
|
||
def find_or_create_form(name, code):
|
||
"""查找或创建表单,返回 (form_id, update_count, code)
|
||
|
||
策略:先 add → 成功则查找 ID;add 失败(code已存在)则查找已有表单。
|
||
结果自动缓存。
|
||
"""
|
||
# 1. 尝试创建
|
||
try:
|
||
add_r = api_request('/desform/add', {'desformName': name, 'desformCode': code})
|
||
if add_r.get('success'):
|
||
# add 成功,优先从返回值获取 ID
|
||
if add_r.get('result') and add_r['result'].get('id'):
|
||
fid = add_r['result']['id']
|
||
_cache_put(code, fid, 0)
|
||
return fid, 0, code
|
||
# 旧版后端不返回 ID,通过 list 搜索
|
||
for wait in [2, 2, 3]:
|
||
time.sleep(wait)
|
||
fid, uc = _find_by_list(code)
|
||
if fid:
|
||
_cache_put(code, fid, uc)
|
||
return fid, uc, code
|
||
except Exception:
|
||
pass
|
||
|
||
# 3. add 失败(code已存在),查找已有表单直接使用
|
||
fid, uc = get_form_id(code)
|
||
if fid:
|
||
return fid, uc, code
|
||
|
||
raise RuntimeError(f'无法查找或创建表单: {code}')
|
||
|
||
|
||
def get_form_fields(form_code):
|
||
"""查询已有表单的字段信息,返回 (titleField_model, {name: {model, key, type}})"""
|
||
# 优先使用缓存获取 ID
|
||
fid, _ = get_form_id(form_code)
|
||
q = None
|
||
if fid:
|
||
q = api_request(f'/desform/queryById?id={fid}', method='GET')
|
||
if not q or not q.get('success') or not q.get('result') or not q['result'].get('desformDesignJson'):
|
||
# fallback: queryByCode
|
||
q = api_request(f'/desform/queryByCode?desformCode={form_code}', method='GET')
|
||
if not q.get('success') or not q.get('result') or not q['result'].get('desformDesignJson'):
|
||
raise RuntimeError(f'表单 {form_code} 未找到或无设计数据')
|
||
design = json.loads(q['result']['desformDesignJson'])
|
||
title_field = design['config']['titleField']
|
||
fields = {}
|
||
def extract(items):
|
||
for item in items:
|
||
if item.get('type') == 'card' and 'list' in item:
|
||
extract(item['list'])
|
||
elif item.get('type') == 'sub-table-design' and 'columns' in item:
|
||
for col in item['columns']:
|
||
extract(col.get('list', []))
|
||
elif 'model' in item and item.get('type') not in ('card',):
|
||
fields[item['name']] = {
|
||
'model': item['model'],
|
||
'key': item['key'],
|
||
'type': item['type']
|
||
}
|
||
extract(design.get('list', []))
|
||
return title_field, fields
|
||
|
||
|
||
# ============================================================
|
||
# ID 生成
|
||
# ============================================================
|
||
def _gen_key():
|
||
ts = int(time.time() * 1000)
|
||
rnd = random.randint(100000, 999999)
|
||
return f"{ts}_{rnd}"
|
||
|
||
|
||
def _gen_model(widget_type):
|
||
ts = int(time.time() * 1000)
|
||
rnd = random.randint(100000, 999999)
|
||
safe = widget_type.replace('-', '_')
|
||
return f"{safe}_{ts}_{rnd}"
|
||
|
||
|
||
def _sleep():
|
||
time.sleep(0.003)
|
||
|
||
|
||
# ============================================================
|
||
# 控件核心工厂
|
||
# ============================================================
|
||
def _adv(fmt='string', custom=False, split=''):
|
||
return {
|
||
"defaultValue": {
|
||
"type": "compose", "value": "", "format": fmt,
|
||
"allowFunc": True, "valueSplit": split, "customConfig": custom
|
||
}
|
||
}
|
||
|
||
|
||
def make_widget(widget_type, name, class_name, icon, options,
|
||
required=False, is_sub=False, parent_key=None, extra=None):
|
||
"""创建控件(通用工厂),返回 (widget_dict, key, model)"""
|
||
key = _gen_key()
|
||
model = _gen_model(widget_type)
|
||
_sleep()
|
||
|
||
fmt = "number" if widget_type in ("number", "integer", "money", "slider") else "string"
|
||
custom = widget_type in ("radio", "checkbox", "select", "link-record", "sub-table-design")
|
||
split = "," if custom else ""
|
||
|
||
w = {
|
||
"type": widget_type, "name": name,
|
||
"className": class_name, "icon": icon,
|
||
"hideTitle": False, "options": options,
|
||
"remoteAPI": {"url": "", "executed": False},
|
||
"key": key, "model": model, "modelType": "main",
|
||
"rules": [{"required": True, "message": "${title}必须填写"}] if required else [],
|
||
"isSubItem": is_sub
|
||
}
|
||
|
||
if widget_type != "link-field":
|
||
w["advancedSetting"] = _adv(fmt, custom, split)
|
||
|
||
if is_sub and parent_key:
|
||
w["subOptions"] = {"width": "200px", "parentKey": parent_key}
|
||
|
||
if extra:
|
||
w.update(extra)
|
||
|
||
return w, key, model
|
||
|
||
|
||
# ============================================================
|
||
# Card 容器
|
||
# ============================================================
|
||
def make_card(*widgets):
|
||
"""创建 card 容器,包裹 1~2 个控件(半行布局时放 2 个)"""
|
||
key = _gen_key()
|
||
_sleep()
|
||
return {
|
||
"key": key, "type": "card", "isAutoGrid": True,
|
||
"isContainer": True, "list": list(widgets),
|
||
"options": {}, "model": f"card_{key}"
|
||
}
|
||
|
||
|
||
# ============================================================
|
||
# 子表
|
||
# ============================================================
|
||
def make_sub_table(name, sub_widgets):
|
||
"""创建子表容器,sub_widgets 为子控件列表,返回 (sub_table_dict, sub_key)"""
|
||
key = _gen_key()
|
||
model = _gen_model("sub-table-design")
|
||
_sleep()
|
||
return {
|
||
"type": "sub-table-design", "name": name,
|
||
"className": "form-sub-table", "icon": "icon-table",
|
||
"hideTitle": False, "class": ["data-j-editable-design"],
|
||
"isContainer": True,
|
||
"columns": [{"span": 24, "list": sub_widgets}],
|
||
"options": {
|
||
"isWordStyle": False, "isWordInnerGrid": False, "gutter": 0,
|
||
"columnNumber": 2, "operationMode": 1, "justify": "start", "align": "top",
|
||
"defaultValue": [], "subTableName": "", "defaultRows": 0,
|
||
"showCheckbox": True, "showNumber": True, "showRowButton": False,
|
||
"allowAdd": True, "autoHeight": True, "defaultValType": "none",
|
||
"hidden": False, "hiddenOnAdd": False, "required": False, "fieldNote": ""
|
||
},
|
||
"advancedSetting": _adv("string", True, ""),
|
||
"key": key, "model": model, "modelType": "main",
|
||
"rules": [], "isSubItem": False
|
||
}, key
|
||
|
||
|
||
# ============================================================
|
||
# 快捷控件工厂函数(大写命名,直接返回 card 包裹的控件)
|
||
# 每个函数返回 (card_dict, widget_key, widget_model)
|
||
# ============================================================
|
||
|
||
def _card_wrap(w, key, model):
|
||
"""包裹控件到 card 并返回 (card, key, model)"""
|
||
return make_card(w), key, model
|
||
|
||
|
||
def INPUT(name, required=False, width=100, placeholder='', unique=False, **kw):
|
||
w, k, m = make_widget("input", name, "form-input", "icon-input", {
|
||
"width": "100%", "defaultValue": "", "required": required,
|
||
"dataType": None, "pattern": "", "patternMessage": "",
|
||
"placeholder": placeholder, "clearable": False, "readonly": False,
|
||
"disabled": False, "fillRuleCode": "", "showPassword": False,
|
||
"unique": unique, "hidden": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def TEXTAREA(name, required=False, width=100, **kw):
|
||
w, k, m = make_widget("textarea", name, "form-textarea", "icon-textarea", {
|
||
"width": "100%", "defaultValue": "", "required": required,
|
||
"disabled": False, "pattern": "", "patternMessage": "",
|
||
"placeholder": "", "readonly": False, "unique": False,
|
||
"hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def NUMBER(name, required=False, width=100, unit='', precision=None, **kw):
|
||
w, k, m = make_widget("number", name, "form-number", "icon-number", {
|
||
"width": "", "required": required, "defaultValue": 0,
|
||
"placeholder": "", "controls": False,
|
||
"min": 0, "minUnlimited": True, "max": 100, "maxUnlimited": True,
|
||
"step": 1, "disabled": False, "controlsPosition": "right",
|
||
"unitText": unit, "unitPosition": "suffix", "showPercent": False,
|
||
"align": "left", "hidden": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def INTEGER(name, required=False, width=100, unit='', **kw):
|
||
w, k, m = make_widget("integer", name, "form-integer", "icon-integer", {
|
||
"width": "", "placeholder": "请输入整数", "required": required,
|
||
"min": 0, "minUnlimited": True, "max": 100, "maxUnlimited": True,
|
||
"step": 1, "precision": 0, "controls": True, "disabled": False,
|
||
"controlsPosition": "right", "unitText": unit, "unitPosition": "suffix",
|
||
"showPercent": False, "align": "left", "hidden": False,
|
||
"hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def MONEY(name, required=False, width=100, unit='元', **kw):
|
||
w, k, m = make_widget("money", name, "form-money", "icon-money", {
|
||
"width": "180px", "placeholder": "请输入金额", "required": required,
|
||
"unitText": unit, "unitPosition": "suffix", "precision": 2,
|
||
"hidden": False, "disabled": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def DATE(name, required=False, width=100, fmt='yyyy-MM-dd', **kw):
|
||
w, k, m = make_widget("date", name, "form-date", "icon-date", {
|
||
"defaultValue": "", "defaultValueType": 1,
|
||
"readonly": False, "disabled": False, "editable": True,
|
||
"clearable": True, "placeholder": "", "startPlaceholder": "",
|
||
"endPlaceholder": "", "designType": "date", "type": "date",
|
||
"format": fmt, "timestamp": True, "required": required,
|
||
"width": "", "hidden": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def TIME(name, required=False, width=100, **kw):
|
||
w, k, m = make_widget("time", name, "form-time", "icon-time", {
|
||
"defaultValue": "", "inputDefVal": False,
|
||
"readonly": False, "disabled": False, "editable": True,
|
||
"clearable": True, "placeholder": "", "startPlaceholder": "",
|
||
"endPlaceholder": "", "isRange": False, "arrowControl": False,
|
||
"format": "HH:mm:ss", "required": required, "width": "",
|
||
"hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def SWITCH(name, active='Y', inactive='N', width=100, **kw):
|
||
w, k, m = make_widget("switch", name, "form-switch", "icon-switch", {
|
||
"defaultValue": False, "disabled": False,
|
||
"activeValue": active, "inactiveValue": inactive,
|
||
"hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def _make_options_list(options, colors=None):
|
||
"""将简单字符串列表转为 options 数组"""
|
||
default_colors = ["#2196F3", "#08C9C9", "#00C345", "#FF9800", "#9C27B0", "#795548", "#607D8B", "#E91E63"]
|
||
result = []
|
||
for i, opt in enumerate(options):
|
||
c = (colors[i] if colors and i < len(colors)
|
||
else default_colors[i % len(default_colors)])
|
||
result.append({"value": opt, "itemColor": c})
|
||
return result
|
||
|
||
|
||
def RADIO(name, options, required=False, width=100, dict_code=None, **kw):
|
||
"""单选框组。options: 字符串列表 或 dict_code 指定系统字典"""
|
||
opts = {
|
||
"inline": True, "matrixWidth": 120, "defaultValue": "",
|
||
"showType": "default", "showLabel": False, "useColor": False,
|
||
"colorIteratorIndex": 3,
|
||
"options": _make_options_list(options) if options else [],
|
||
"required": required, "width": "", "remote": False,
|
||
"remoteOptions": [], "props": {"value": "value", "label": "label"},
|
||
"remoteFunc": "", "disabled": False, "hidden": False,
|
||
"hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}
|
||
extra = {}
|
||
if dict_code:
|
||
opts["remote"] = "dict"
|
||
opts["dictCode"] = dict_code
|
||
opts["showLabel"] = True
|
||
opts["options"] = []
|
||
extra["dictOptions"] = options if isinstance(options[0], dict) else []
|
||
w, k, m = make_widget("radio", name, "form-radio", "icon-radio-active", opts,
|
||
required=required, extra=extra, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def CHECKBOX(name, options, required=False, width=100, dict_code=None, **kw):
|
||
opts = {
|
||
"inline": True, "matrixWidth": 120, "defaultValue": [],
|
||
"showLabel": False, "showType": "default", "useColor": False,
|
||
"colorIteratorIndex": 3,
|
||
"options": _make_options_list(options) if options else [],
|
||
"required": required, "width": "", "remote": False,
|
||
"remoteOptions": [], "props": {"value": "value", "label": "label"},
|
||
"remoteFunc": "", "disabled": False, "hidden": False,
|
||
"hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}
|
||
extra = {}
|
||
if dict_code:
|
||
opts["remote"] = "dict"
|
||
opts["dictCode"] = dict_code
|
||
opts["showLabel"] = True
|
||
opts["options"] = []
|
||
extra["dictOptions"] = options if isinstance(options[0], dict) else []
|
||
w, k, m = make_widget("checkbox", name, "form-checkbox", "icon-checkbox", opts,
|
||
required=required, extra=extra, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def SELECT(name, options, required=False, width=100, multiple=False, dict_code=None, **kw):
|
||
opts = {
|
||
"defaultValue": "" if not multiple else [],
|
||
"multiple": multiple, "disabled": False, "clearable": True,
|
||
"placeholder": "", "required": required, "showLabel": False,
|
||
"showType": "default", "width": "", "useColor": False,
|
||
"colorIteratorIndex": 3,
|
||
"options": _make_options_list(options) if options else [],
|
||
"remote": False, "filterable": False,
|
||
"remoteOptions": [], "props": {"value": "value", "label": "label"},
|
||
"remoteFunc": "", "hidden": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}
|
||
extra = {}
|
||
if dict_code:
|
||
opts["remote"] = "dict"
|
||
opts["dictCode"] = dict_code
|
||
opts["showLabel"] = True
|
||
opts["options"] = []
|
||
extra["dictOptions"] = options if isinstance(options[0], dict) else []
|
||
w, k, m = make_widget("select", name, "form-select", "icon-select", opts,
|
||
required=required, extra=extra, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def PHONE(name, required=False, width=100, **kw):
|
||
w, k, m = make_widget("phone", name, "form-input-phone", "icon-mobile-phone", {
|
||
"width": "300px", "defaultValue": "", "required": required,
|
||
"placeholder": "", "readonly": False, "disabled": False,
|
||
"unique": False, "hidden": False, "showVerifyCode": False,
|
||
"hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}, required=required, extra={
|
||
"defaultRules": [
|
||
{"type": "phone", "message": "请输入正确的手机号码"},
|
||
{"type": "validator", "message": "", "trigger": "blur"}
|
||
]
|
||
}, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def EMAIL(name, required=False, width=100, **kw):
|
||
w, k, m = make_widget("email", name, "form-input-email", "icon-email", {
|
||
"width": "300px", "defaultValue": "", "required": required,
|
||
"placeholder": "", "readonly": False, "disabled": False,
|
||
"unique": False, "hidden": False, "showVerifyCode": False,
|
||
"hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}, required=required, extra={
|
||
"defaultRules": [
|
||
{"type": "email", "message": "请输入正确的邮箱地址"},
|
||
{"type": "validator", "message": "", "trigger": "blur"}
|
||
]
|
||
}, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def USER(name, required=False, width=100, multiple=False, default_login=False, **kw):
|
||
w, k, m = make_widget("select-user", name, "form-select-user", "icon-user-circle", {
|
||
"keyMaps": [], "defaultValue": "", "defaultLogin": default_login,
|
||
"placeholder": "", "width": "100%", "multiple": multiple,
|
||
"disabled": False, "customReturnField": "username",
|
||
"hidden": False, "dataAuthType": "member",
|
||
"hiddenOnAdd": False, "required": required, "fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def DEPART(name, required=False, width=100, multiple=False, **kw):
|
||
w, k, m = make_widget("select-depart", name, "form-select-depart", "icon-depart", {
|
||
"keyMaps": [], "defaultValue": "", "defaultLogin": False,
|
||
"placeholder": "", "width": "100%", "multiple": multiple,
|
||
"disabled": False, "customReturnField": "id",
|
||
"hidden": False, "dataAuthType": "member",
|
||
"hiddenOnAdd": False, "required": required, "fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def AREA(name, required=False, width=100, level=3, **kw):
|
||
w, k, m = make_widget("area-linkage", name, "form-area-linkage", "icon-jilianxuanze", {
|
||
"width": "", "placeholder": "请选择", "areaLevel": level,
|
||
"defaultValue": "", "clearable": True, "disabled": False,
|
||
"hidden": False, "hiddenOnAdd": False, "required": required,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def IMGUPLOAD(name, required=False, width=100, length=9, **kw):
|
||
w, k, m = make_widget("imgupload", name, "form-tupian", "icon-tupian", {
|
||
"defaultValue": [], "size": {"width": 100, "height": 100},
|
||
"width": "", "tokenFunc": "funcGetToken", "token": "",
|
||
"domain": "http://img.h5huodong.com", "disabled": False,
|
||
"length": length, "multiple": True, "hidden": False,
|
||
"hiddenOnAdd": False, "required": required,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def FILE(name, required=False, width=100, **kw):
|
||
w, k, m = make_widget("file-upload", name, "form-file-upload", "icon-shangchuan", {
|
||
"defaultValue": [], "token": "", "length": 0,
|
||
"drag": False, "listStyleType": "card", "multiple": False,
|
||
"multipleDown": True, "disabled": False, "buttonText": "点击上传文件",
|
||
"tokenFunc": "funcGetToken", "hidden": False, "hiddenOnAdd": False,
|
||
"required": required, "fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def SLIDER(name, required=False, width=100, min_val=0, max_val=100, show_input=False, **kw):
|
||
w, k, m = make_widget("slider", name, "form-slider", "icon-slider", {
|
||
"defaultValue": 0, "disabled": False, "required": required,
|
||
"min": min_val, "max": max_val, "step": 1,
|
||
"showInput": show_input, "showPercent": False, "range": False,
|
||
"width": "", "hidden": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def RATE(name, required=False, width=100, max_val=5, **kw):
|
||
w, k, m = make_widget("rate", name, "form-rate", "icon-rate", {
|
||
"defaultValue": 0, "max": max_val, "disabled": False,
|
||
"allowHalf": False, "required": required,
|
||
"hidden": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, extra={
|
||
"defaultRules": [{"type": "validator", "message": "", "trigger": "change"}]
|
||
}, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def COLOR(name, width=100, **kw):
|
||
w, k, m = make_widget("color", name, "form-color", "icon-color", {
|
||
"defaultValue": "", "disabled": False, "showAlpha": False,
|
||
"required": False, "hidden": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def AUTONUMBER(name, prefix='', width=100, **kw):
|
||
"""自动编号控件"""
|
||
rules = [{"type": "number", "mode": 1, "start": 1, "reset": 0, "length": 4, "continue": False}]
|
||
if prefix:
|
||
rules.insert(0, {"type": "text", "value": prefix})
|
||
rules.insert(1, {"type": "date", "format": "yyyyMMdd"})
|
||
w, k, m = make_widget("auto-number", name, "form-auto-number", "icon-hashtag", {
|
||
"numberRules": rules,
|
||
"generateOnAdd": True,
|
||
"placeholder": "自动生成,不需要填写",
|
||
"hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def HANDSIGN(name, required=False, width=100, **kw):
|
||
w, k, m = make_widget("hand-sign", name, "form-hand-sign", "icon-qianming", {
|
||
"width": "100%", "height": "200px", "disabled": False,
|
||
"hidden": False, "hiddenOnAdd": False, "required": required,
|
||
"fieldNote": "", "autoWidth": width
|
||
}, required=required, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
# ============================================================
|
||
# 不需要 card 包裹的控件(直接返回控件本身)
|
||
# ============================================================
|
||
|
||
def DIVIDER(text='', **kw):
|
||
"""分隔符(不需要 card 包裹),返回 (widget_dict, key, model)"""
|
||
w, k, m = make_widget("divider", text or "分隔符", "form-divider", "icon-divider", {
|
||
"heightNumber": 48, "type": "horizontal", "text": text,
|
||
"position": "center", "hidden": False, "hiddenOnAdd": False,
|
||
"required": False, "fieldNote": ""
|
||
}, **kw)
|
||
w["hideLabel"] = True
|
||
w["formItemMargin"] = True
|
||
return w, k, m
|
||
|
||
|
||
def EDITOR(name, required=False, **kw):
|
||
"""富文本编辑器(不需要 card 包裹)"""
|
||
w, k, m = make_widget("editor", name, "form-editor", "icon-fuwenbenkuang", {
|
||
"defaultValue": "", "width": "100%", "disabled": False,
|
||
"hidden": False, "hiddenOnAdd": False, "required": required, "fieldNote": ""
|
||
}, required=required, **kw)
|
||
return w, k, m
|
||
|
||
|
||
def MARKDOWN(name, required=False, **kw):
|
||
"""Markdown 编辑器(不需要 card 包裹)"""
|
||
w, k, m = make_widget("markdown", name, "form-markdown", "icon-markdown", {
|
||
"defaultValue": "", "width": "100%", "height": 300,
|
||
"viewerAutoHeight": False, "disabled": False,
|
||
"hidden": False, "hiddenOnAdd": False, "required": required, "fieldNote": ""
|
||
}, required=required, **kw)
|
||
return w, k, m
|
||
|
||
|
||
# ============================================================
|
||
# 关联控件
|
||
# ============================================================
|
||
|
||
def LINK_RECORD(name, source_code, title_field, show_fields=None,
|
||
required=False, width=100, show_mode='single', show_type='card',
|
||
is_sub=False, parent_key=None, **kw):
|
||
"""关联记录控件,返回 (card_or_widget, key, model)
|
||
|
||
Args:
|
||
source_code: 源表单 desformCode
|
||
title_field: 源表单标题字段 model
|
||
show_fields: 源表单展示字段 model 列表
|
||
show_mode: 'single' 或 'many'
|
||
show_type: 'card', 'select', 'table'
|
||
"""
|
||
opts = {
|
||
"sourceCode": source_code, "showMode": show_mode, "showType": show_type,
|
||
"titleField": title_field, "showFields": show_fields or [],
|
||
"allowView": True, "allowEdit": True, "allowAdd": True, "allowSelect": True,
|
||
"buttonText": "添加记录", "twoWayModel": "", "dataSelectAuth": "all",
|
||
"filters": [{"matchType": "AND", "rules": []}],
|
||
"search": {"enabled": False, "field": "", "rule": "like", "afterShow": False, "fields": []},
|
||
"createMode": {"add": True, "select": False, "params": {"selectLinkModel": ""}},
|
||
"width": "100%", "defaultValue": "", "defaultValType": "none",
|
||
"required": required, "disabled": False, "hidden": False,
|
||
"isSubTable": False, "isSelf": False,
|
||
"hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}
|
||
w, k, m = make_widget("link-record", name, "form-link-record", "icon-link", opts,
|
||
required=required, is_sub=is_sub, parent_key=parent_key, **kw)
|
||
# 需要 card 包裹(除非 showMode=many 或 showType=table)
|
||
if show_mode == 'single' and show_type != 'table':
|
||
return _card_wrap(w, k, m)
|
||
return w, k, m
|
||
|
||
|
||
def LINK_FIELD(name, link_record_key, show_field, field_type='input',
|
||
field_options=None, width=100, is_sub=False, parent_key=None, **kw):
|
||
"""他表字段控件(与 link-record 配对使用)"""
|
||
opts = {
|
||
"linkRecordKey": link_record_key, "showField": show_field,
|
||
"saveType": "view", "fieldType": field_type,
|
||
"fieldOptions": field_options or {},
|
||
"width": "100%", "defaultValue": "", "readonly": False,
|
||
"disabled": False, "hidden": False, "hiddenOnAdd": False,
|
||
"fieldNote": "", "autoWidth": width
|
||
}
|
||
w, k, m = make_widget("link-field", name, "form-link-field", "icon-field", opts,
|
||
is_sub=is_sub, parent_key=parent_key, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
def FORMULA(name, mode='custom', expression='', fields=None,
|
||
width=100, unit='', decimal=2, **kw):
|
||
"""公式控件
|
||
|
||
Args:
|
||
mode: 'SUM', 'avg', 'max', 'min', 'product', 'custom'
|
||
expression: 自定义表达式(mode='custom' 时使用)
|
||
fields: SUM/avg 等模式时的字段 model 列表
|
||
"""
|
||
opts = {
|
||
"type": "number", "mode": mode, "expression": expression,
|
||
"decimal": decimal, "thousand": True, "percent": False,
|
||
"unitPosition": "suffix", "unitText": unit, "emptyAsZero": True,
|
||
"dateBegin": "", "dateEnd": "", "dateFormatMethod": 1,
|
||
"datePrintUnit": "m", "dateAddExp": "", "datePrintFormat": "YYYY-MM-DD",
|
||
"hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
|
||
}
|
||
if fields and mode != 'custom':
|
||
opts["fields"] = fields
|
||
w, k, m = make_widget("formula", name, "form-formula", "icon-gongshibianji", opts, **kw)
|
||
return _card_wrap(w, k, m)
|
||
|
||
|
||
# ============================================================
|
||
# 子表内控件(不需要 card,返回裸控件)
|
||
# ============================================================
|
||
|
||
def SUB_INPUT(name, parent_key, required=False, col_width='200px'):
|
||
w, k, m = make_widget("input", name, "form-input", "icon-input", {
|
||
"width": "100%", "defaultValue": "", "required": required,
|
||
"dataType": None, "pattern": "", "patternMessage": "",
|
||
"placeholder": "", "clearable": False, "readonly": False,
|
||
"disabled": False, "fillRuleCode": "", "showPassword": False,
|
||
"unique": False, "hidden": False, "hiddenOnAdd": False, "fieldNote": ""
|
||
}, required=required, is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
def SUB_INTEGER(name, parent_key, required=False, col_width='120px', unit=''):
|
||
w, k, m = make_widget("integer", name, "form-integer", "icon-integer", {
|
||
"width": "", "placeholder": "", "required": required,
|
||
"min": 0, "minUnlimited": True, "max": 100, "maxUnlimited": True,
|
||
"step": 1, "precision": 0, "controls": True, "disabled": False,
|
||
"controlsPosition": "right", "unitText": unit, "unitPosition": "suffix",
|
||
"showPercent": False, "align": "left", "hidden": False,
|
||
"hiddenOnAdd": False, "fieldNote": ""
|
||
}, required=required, is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
def SUB_NUMBER(name, parent_key, required=False, col_width='120px', unit=''):
|
||
w, k, m = make_widget("number", name, "form-number", "icon-number", {
|
||
"width": "", "required": required, "defaultValue": 0,
|
||
"placeholder": "", "controls": False,
|
||
"min": 0, "minUnlimited": True, "max": 100, "maxUnlimited": True,
|
||
"step": 1, "disabled": False, "controlsPosition": "right",
|
||
"unitText": unit, "unitPosition": "suffix", "showPercent": False,
|
||
"align": "left", "hidden": False, "hiddenOnAdd": False, "fieldNote": ""
|
||
}, required=required, is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
def SUB_MONEY(name, parent_key, required=False, col_width='150px', unit='元'):
|
||
w, k, m = make_widget("money", name, "form-money", "icon-money", {
|
||
"width": "180px", "placeholder": "", "required": required,
|
||
"unitText": unit, "unitPosition": "suffix", "precision": 2,
|
||
"hidden": False, "disabled": False, "hiddenOnAdd": False, "fieldNote": ""
|
||
}, required=required, is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
def SUB_SELECT(name, parent_key, options, required=False, col_width='150px'):
|
||
w, k, m = make_widget("select", name, "form-select", "icon-select", {
|
||
"defaultValue": "", "multiple": False, "disabled": False, "clearable": True,
|
||
"placeholder": "", "required": required, "showLabel": False,
|
||
"showType": "default", "width": "", "useColor": False, "colorIteratorIndex": 3,
|
||
"options": _make_options_list(options),
|
||
"remote": False, "filterable": False,
|
||
"remoteOptions": [], "props": {"value": "value", "label": "label"},
|
||
"remoteFunc": "", "hidden": False, "hiddenOnAdd": False, "fieldNote": ""
|
||
}, required=required, is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
def SUB_DATE(name, parent_key, required=False, col_width='180px'):
|
||
w, k, m = make_widget("date", name, "form-date", "icon-date", {
|
||
"defaultValue": "", "defaultValueType": 1,
|
||
"readonly": False, "disabled": False, "editable": True,
|
||
"clearable": True, "placeholder": "", "startPlaceholder": "",
|
||
"endPlaceholder": "", "designType": "date", "type": "date",
|
||
"format": "yyyy-MM-dd", "timestamp": True, "required": required,
|
||
"width": "", "hidden": False, "hiddenOnAdd": False, "fieldNote": ""
|
||
}, required=required, is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
def SUB_LINK_RECORD(name, parent_key, source_code, title_field, show_fields=None,
|
||
required=False, col_width='200px'):
|
||
opts = {
|
||
"sourceCode": source_code, "showMode": "single", "showType": "card",
|
||
"titleField": title_field, "showFields": show_fields or [],
|
||
"allowView": True, "allowEdit": True, "allowAdd": True, "allowSelect": True,
|
||
"buttonText": "添加记录", "twoWayModel": "", "dataSelectAuth": "all",
|
||
"filters": [{"matchType": "AND", "rules": []}],
|
||
"search": {"enabled": False, "field": "", "rule": "like", "afterShow": False, "fields": []},
|
||
"createMode": {"add": True, "select": False, "params": {"selectLinkModel": ""}},
|
||
"width": "100%", "defaultValue": "", "defaultValType": "none",
|
||
"required": required, "disabled": False, "hidden": False,
|
||
"isSubTable": False, "isSelf": False,
|
||
"hiddenOnAdd": False, "fieldNote": ""
|
||
}
|
||
w, k, m = make_widget("link-record", name, "form-link-record", "icon-link", opts,
|
||
required=required, is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
def SUB_LINK_FIELD(name, parent_key, link_record_key, show_field,
|
||
field_type='input', field_options=None, col_width='150px'):
|
||
opts = {
|
||
"linkRecordKey": link_record_key, "showField": show_field,
|
||
"saveType": "view", "fieldType": field_type,
|
||
"fieldOptions": field_options or {},
|
||
"width": "100%", "defaultValue": "", "readonly": False,
|
||
"disabled": False, "hidden": False, "hiddenOnAdd": False, "fieldNote": ""
|
||
}
|
||
w, k, m = make_widget("link-field", name, "form-link-field", "icon-field", opts,
|
||
is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
def SUB_FORMULA(name, parent_key, mode='custom', expression='', col_width='150px', unit=''):
|
||
opts = {
|
||
"type": "number", "mode": mode, "expression": expression,
|
||
"decimal": 2, "thousand": True, "percent": False,
|
||
"unitPosition": "suffix", "unitText": unit, "emptyAsZero": True,
|
||
"dateBegin": "", "dateEnd": "", "dateFormatMethod": 1,
|
||
"datePrintUnit": "m", "dateAddExp": "", "datePrintFormat": "YYYY-MM-DD",
|
||
"hidden": False, "hiddenOnAdd": False, "fieldNote": ""
|
||
}
|
||
w, k, m = make_widget("formula", name, "form-formula", "icon-gongshibianji", opts,
|
||
is_sub=True, parent_key=parent_key)
|
||
w["subOptions"]["width"] = col_width
|
||
return w, k, m
|
||
|
||
|
||
# ============================================================
|
||
# 设计 JSON 组装 & 保存
|
||
# ============================================================
|
||
|
||
def _collect_types(items):
|
||
"""递归收集所有控件类型"""
|
||
types = set()
|
||
for item in items:
|
||
t = item.get('type')
|
||
if t:
|
||
types.add(t)
|
||
if t == 'card' and 'list' in item:
|
||
types.update(_collect_types(item['list']))
|
||
if t == 'grid' and 'columns' in item:
|
||
for col in item['columns']:
|
||
types.update(_collect_types(col.get('list', [])))
|
||
if t == 'sub-table-design' and 'columns' in item:
|
||
for col in item['columns']:
|
||
types.update(_collect_types(col.get('list', [])))
|
||
return types
|
||
|
||
|
||
def make_word_grid(*rows, cols_per_row=2):
|
||
"""创建 Word 风格的栅格行(匹配 JeecgBoot 真实 Word 风格实现)
|
||
|
||
真实 Word 风格通过 CSS class `form-grid form-grid-word-theme` + 外部 CSS
|
||
`/desform/expand/css/theme-word.css` 实现表格边框效果。
|
||
|
||
布局规则(参照加班申请等真实表单):
|
||
- 两列行:标签 span=6 + 控件 span=6 + 标签 span=4 + 控件 span=8
|
||
- 单列整行:标签 span=6 + 控件 span=18
|
||
- 标签列使用 `text` 控件(居中、16px),flex 垂直居中
|
||
- 控件列的控件 hideTitle=True
|
||
|
||
Args:
|
||
rows: 每个 row 是一组 (widget_dict, key, model) tuple 或裸 widget dict
|
||
cols_per_row: 每行放几个控件(1 或 2)
|
||
|
||
Returns:
|
||
grid dict
|
||
"""
|
||
key = _gen_key()
|
||
_sleep()
|
||
|
||
columns = []
|
||
|
||
for idx, item in enumerate(rows):
|
||
if isinstance(item, tuple):
|
||
w = item[0]
|
||
else:
|
||
w = item
|
||
|
||
# 从 card 中提取内部控件
|
||
inner = w
|
||
if w.get('type') == 'card' and w.get('list') and len(w['list']) == 1:
|
||
inner = w['list'][0]
|
||
|
||
widget_name = inner.get('name', '')
|
||
inner['hideTitle'] = True
|
||
inner['hideLabel'] = True
|
||
|
||
# 计算 span
|
||
if cols_per_row == 2:
|
||
if idx == 0:
|
||
label_span, field_span = 6, 6
|
||
else:
|
||
label_span, field_span = 4, 8
|
||
else:
|
||
label_span, field_span = 6, 18
|
||
|
||
# 标题列(text 控件,居中 16px)
|
||
label_key = _gen_key()
|
||
_sleep()
|
||
label_widget = {
|
||
"type": "text", "name": "文本",
|
||
"className": "form-text", "icon": "icon-text",
|
||
"hideLabel": True, "hideTitle": False,
|
||
"options": {
|
||
"text": widget_name, "width": "100%", "align": "center",
|
||
"fontSize": 16, "fontColor": "#000000",
|
||
"fontBold": False, "fontItalic": False,
|
||
"fontUnderline": False, "fontLineThrough": False,
|
||
"hidden": False, "required": False, "hiddenOnAdd": False,
|
||
"verticalAlign": "top", "fieldNote": ""
|
||
},
|
||
"remoteAPI": {"url": "", "executed": False},
|
||
"key": label_key, "model": f"text_{label_key}",
|
||
"modelType": "main", "rules": [], "isSubItem": False
|
||
}
|
||
|
||
# 标签列 options:flex 垂直居中(textarea/hand-sign 等宽控件也居中)
|
||
label_col_opts = {"flex": True, "flexAlignItems": "center", "flexJustifyContent": "start"}
|
||
|
||
columns.append({
|
||
"span": label_span,
|
||
"list": [label_widget],
|
||
"options": label_col_opts
|
||
})
|
||
|
||
# 控件列
|
||
columns.append({
|
||
"span": field_span,
|
||
"list": [inner],
|
||
"options": {"flex": True, "flexAlignItems": "center", "flexJustifyContent": "start"}
|
||
})
|
||
|
||
grid = {
|
||
"type": "grid", "name": "栅格布局",
|
||
"className": "form-grid form-grid-word-theme",
|
||
"icon": "icon-grid",
|
||
"hideLabel": True, "isContainer": True,
|
||
"columns": columns,
|
||
"options": {
|
||
"gutter": 0, "justify": "start", "align": "top",
|
||
"hidden": False, "required": False, "hiddenOnAdd": False,
|
||
"isWordStyle": False, "isWordInnerGrid": False, "fieldNote": ""
|
||
},
|
||
"key": key, "model": f"grid_{key}",
|
||
"rules": [], "hideTitle": False, "modelType": "main"
|
||
}
|
||
return grid
|
||
|
||
|
||
def _make_word_title(title_text):
|
||
"""创建 Word 风格表单顶部标题(text 控件,24px 加粗居中)"""
|
||
key = _gen_key()
|
||
_sleep()
|
||
return {
|
||
"type": "text", "name": "文本",
|
||
"className": "form-text", "icon": "icon-text",
|
||
"hideLabel": True, "hideTitle": False,
|
||
"options": {
|
||
"text": title_text, "width": "100%", "align": "center",
|
||
"fontSize": 24, "fontColor": "#000000",
|
||
"fontBold": True, "fontItalic": False,
|
||
"fontUnderline": False, "fontLineThrough": False,
|
||
"hidden": False, "required": False, "hiddenOnAdd": False,
|
||
"verticalAlign": "top", "fieldNote": ""
|
||
},
|
||
"remoteAPI": {"url": "", "executed": False},
|
||
"key": key, "model": f"text_{key}",
|
||
"modelType": "main", "rules": [], "isSubItem": False
|
||
}
|
||
|
||
|
||
def _apply_word_layout(widgets, form_name=''):
|
||
"""将 widgets 列表转换为 Word 风格布局(匹配 JeecgBoot 真实实现)
|
||
|
||
真实 Word 风格特征:
|
||
- 顶部有独立的 text 标题控件(24px 加粗居中)
|
||
- 每行是 grid 容器,className = 'form-grid form-grid-word-theme'
|
||
- 适合半行的控件两两配对(标签6+值6 | 标签4+值8)
|
||
- textarea/file-upload/hand-sign 等宽控件独占一行(标签6+值18)
|
||
- formStyle 保持 'normal',样式由外部 CSS theme-word.css 驱动
|
||
|
||
Args:
|
||
widgets: 控件列表
|
||
form_name: 表单名称(用于生成顶部标题,空则不生成)
|
||
|
||
Returns:
|
||
(new_top_items, all_models)
|
||
"""
|
||
top_items = []
|
||
all_models = []
|
||
|
||
# 添加顶部标题
|
||
if form_name:
|
||
title_widget = _make_word_title(form_name)
|
||
top_items.append(title_widget)
|
||
|
||
half_buffer = None
|
||
|
||
for item in widgets:
|
||
wtype = _get_widget_type(item)
|
||
|
||
if isinstance(item, tuple):
|
||
key, model = item[1], item[2]
|
||
else:
|
||
key, model = item.get('key', ''), item.get('model', '')
|
||
|
||
if _is_half_suitable(wtype):
|
||
if half_buffer is None:
|
||
half_buffer = (item, key, model)
|
||
else:
|
||
# 两个控件配对成一行
|
||
grid = make_word_grid(half_buffer[0], item, cols_per_row=2)
|
||
top_items.append(grid)
|
||
all_models.append((half_buffer[1], half_buffer[2]))
|
||
all_models.append((key, model))
|
||
half_buffer = None
|
||
else:
|
||
# 先刷出缓冲区
|
||
if half_buffer is not None:
|
||
grid = make_word_grid(half_buffer[0], cols_per_row=1)
|
||
top_items.append(grid)
|
||
all_models.append((half_buffer[1], half_buffer[2]))
|
||
half_buffer = None
|
||
|
||
# 宽控件独占一行
|
||
grid = make_word_grid(item, cols_per_row=1)
|
||
top_items.append(grid)
|
||
all_models.append((key, model))
|
||
|
||
if half_buffer is not None:
|
||
grid = make_word_grid(half_buffer[0], cols_per_row=1)
|
||
top_items.append(grid)
|
||
all_models.append((half_buffer[1], half_buffer[2]))
|
||
|
||
return top_items, all_models
|
||
|
||
|
||
def build_design_json(widgets, title_model, form_style='normal'):
|
||
"""组装完整的 desformDesignJson
|
||
|
||
Args:
|
||
widgets: 顶层控件列表(card 包裹的和不需要 card 的混合)
|
||
title_model: 标题字段的 model
|
||
form_style: 表单风格 'normal' 或 'word'
|
||
- 'normal': 默认风格
|
||
- 'word': Word 风格(formStyle 保持 normal,通过 CSS class + 外部 CSS 实现)
|
||
"""
|
||
is_word = (form_style == 'word')
|
||
has_widgets = sorted(list(_collect_types(widgets)))
|
||
|
||
# Word 风格:加载外部 theme-word.css,关闭自动栅格和顶部标题
|
||
if is_word:
|
||
expand = {"js": "", "css": "", "url": {"js": "", "css": "/desform/expand/css/theme-word.css"}}
|
||
show_header = False
|
||
disabled_auto_grid = True
|
||
dialog_top = 60
|
||
dialog_width = 1100
|
||
allow_print = True
|
||
else:
|
||
expand = {"js": "", "css": "", "url": {"js": "", "css": ""}}
|
||
show_header = True
|
||
disabled_auto_grid = False
|
||
dialog_top = 20
|
||
dialog_width = 1000
|
||
allow_print = False
|
||
|
||
return {
|
||
"list": widgets,
|
||
"config": {
|
||
"formStyle": "word" if is_word else "normal",
|
||
"titleField": title_model,
|
||
"showHeaderTitle": show_header,
|
||
"labelWidth": 100,
|
||
"labelPosition": "top",
|
||
"size": "small",
|
||
"dialogOptions": {
|
||
"top": dialog_top, "width": dialog_width,
|
||
"padding": {"top": 25, "right": 25, "bottom": 30, "left": 25}
|
||
},
|
||
"disabledAutoGrid": disabled_auto_grid,
|
||
"designMobileView": False,
|
||
"enableComment": True,
|
||
"hasWidgets": has_widgets,
|
||
"defaultLoadLargeControls": False,
|
||
"expand": expand,
|
||
"transactional": True,
|
||
"customRequestURL": [{"url": ""}],
|
||
"disableMobileCss": True,
|
||
"allowExternalLink": False,
|
||
"externalLinkShowData": is_word,
|
||
"headerImgUrl": "",
|
||
"externalTitle": "",
|
||
"enableNotice": False,
|
||
"noticeMode": "external",
|
||
"noticeType": "system",
|
||
"noticeReceiver": "",
|
||
"allowPrint": allow_print,
|
||
"allowJmReport": False,
|
||
"jmReportURL": "",
|
||
"bizRuleConfig": [],
|
||
"bigDataMode": False
|
||
}
|
||
}
|
||
|
||
|
||
def save_design(form_id, form_code, widgets, title_model, update_count=1, form_style='normal'):
|
||
"""保存表单设计到 API
|
||
|
||
Args:
|
||
form_id: 表单 ID
|
||
form_code: 表单编码(用于日志)
|
||
widgets: 顶层控件列表
|
||
title_model: 标题字段 model
|
||
update_count: 当前 updateCount(从 find_or_create_form 获取)
|
||
form_style: 表单风格 'normal' 或 'word'
|
||
|
||
Returns:
|
||
API 响应 dict
|
||
"""
|
||
design_json = build_design_json(widgets, title_model, form_style)
|
||
payload = {
|
||
'id': form_id,
|
||
'desformDesignJson': json.dumps(design_json, ensure_ascii=False),
|
||
'updateCount': update_count,
|
||
'autoNumberDesignConfig': {'update': {}, 'current': {}},
|
||
'refTableDefaultValDbSync': {'changes': {}, 'removeKeys': []}
|
||
}
|
||
result = api_request('/desform/edit', payload, method='PUT')
|
||
|
||
if result.get('success'):
|
||
print(f' {form_code} 保存成功')
|
||
return result
|
||
|
||
msg = result.get('message', '')
|
||
|
||
# 自动重试: 未找到对应实体 → ID 可能是旧的幽灵记录,清缓存后重新搜索
|
||
if '未找到对应实体' in msg:
|
||
print(f' {form_code} ID={form_id} 无效,重新搜索...')
|
||
_cache_remove(form_code)
|
||
new_id, new_uc = _find_by_list(form_code)
|
||
if new_id and new_id != form_id:
|
||
payload['id'] = new_id
|
||
payload['updateCount'] = new_uc
|
||
result = api_request('/desform/edit', payload, method='PUT')
|
||
if result.get('success'):
|
||
print(f' {form_code} 保存成功 (重试, ID={new_id})')
|
||
return result
|
||
|
||
# 自动重试: 版本过时 → updateCount 不匹配,逐个尝试 uc+1, uc+2, ...
|
||
if '版本已过时' in msg or '版本过时' in msg:
|
||
print(f' {form_code} 版本过时(uc={update_count}),尝试递增...')
|
||
_cache_remove(form_code)
|
||
for try_uc in range(update_count + 1, update_count + 10):
|
||
payload['updateCount'] = try_uc
|
||
result = api_request('/desform/edit', payload, method='PUT')
|
||
if result.get('success'):
|
||
print(f' {form_code} 保存成功 (uc={try_uc})')
|
||
return result
|
||
retry_msg = result.get('message', '')
|
||
if '版本已过时' not in retry_msg and '版本过时' not in retry_msg:
|
||
break
|
||
|
||
raise RuntimeError(f'{form_code} 保存失败: {msg}')
|
||
|
||
|
||
# ============================================================
|
||
# 菜单 SQL 生成(含授权 SQL)
|
||
# ============================================================
|
||
|
||
def _gen_id():
|
||
"""生成 32 位无横线 UUID 作为菜单/授权记录 ID"""
|
||
return uuid.uuid4().hex
|
||
|
||
|
||
def gen_menu_sql(parent_name, children, role_id=None, icon='ant-design:appstore-outlined'):
|
||
"""生成菜单 + 授权 SQL
|
||
|
||
Args:
|
||
parent_name: 父菜单名称
|
||
children: [(name, desform_code, sort), ...] 或 [(menu_id, name, desform_code, sort), ...] (兼容旧格式)
|
||
role_id: 角色 ID(默认使用全局 ROLE_ID)
|
||
icon: 父菜单图标(默认 'ant-design:appstore-outlined')
|
||
|
||
Returns:
|
||
完整 SQL 字符串
|
||
"""
|
||
rid = role_id or ROLE_ID
|
||
lines = []
|
||
parent_id = _gen_id()
|
||
|
||
# 父菜单
|
||
lines.append(f"""INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
|
||
VALUES ('{parent_id}', NULL, '{parent_name}', '/{parent_id}', 'layouts/RouteView', NULL, NULL, 0, NULL, '1', 1.00, 0, '{icon}', 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', now(), NULL, NULL, 0);""")
|
||
|
||
# 父菜单授权
|
||
lines.append(f"""INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip)
|
||
VALUES ('{_gen_id()}', '{rid}', '{parent_id}', NULL, now(), '127.0.0.1');""")
|
||
|
||
# 子菜单
|
||
for item in children:
|
||
# 兼容旧格式 (menu_id, name, code, sort) 和新格式 (name, code, sort)
|
||
if len(item) == 4:
|
||
_, name, code, sort = item
|
||
else:
|
||
name, code, sort = item
|
||
menu_id = _gen_id()
|
||
lines.append(f"""
|
||
INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
|
||
VALUES ('{menu_id}', '{parent_id}', '{name}', '/online/desform/list/{code}', 'super/online/desform/auto/AutoDesformDataList', 'AutoDesformDataList', NULL, 0, NULL, '1', {sort}.00, 0, NULL, 0, 1, 0, 0, 0, NULL, '1', 0, 0, 'admin', now(), NULL, NULL, 0);""")
|
||
|
||
lines.append(f"""INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip)
|
||
VALUES ('{_gen_id()}', '{rid}', '{menu_id}', NULL, now(), '127.0.0.1');""")
|
||
|
||
return '\n'.join(lines)
|
||
|
||
|
||
# ============================================================
|
||
# 便捷函数:批量创建表单
|
||
# ============================================================
|
||
|
||
def query_form(code):
|
||
"""查询表单基本信息,返回 dict 或 None(带缓存)
|
||
|
||
返回字段包括: id, desformCode, desformName, updateCount, desformDesignJson 等
|
||
"""
|
||
fid, _ = get_form_id(code)
|
||
if fid:
|
||
try:
|
||
r = api_request(f'/desform/queryById?id={fid}', method='GET')
|
||
if r.get('success') and r.get('result'):
|
||
# 更新缓存中的 updateCount
|
||
_cache_put(code, fid, r['result'].get('updateCount', 0))
|
||
return r['result']
|
||
except Exception:
|
||
pass
|
||
return None
|
||
|
||
|
||
def delete_form(code_or_id, form_id=None):
|
||
"""删除表单:逻辑删除 → 真实删除
|
||
|
||
支持 3 种调用方式:
|
||
delete_form('elder_person') # 传 code,自动查找 ID
|
||
delete_form('elder_person', '123456') # 传 code + 已知 ID,跳过搜索
|
||
delete_form(id='123456') # 只传 ID
|
||
|
||
会自动处理同 code 多条记录的情况(全部删除)。
|
||
删除后自动清除缓存。返回已删除的 ID 列表。
|
||
"""
|
||
deleted_ids = []
|
||
code = None
|
||
|
||
# 判断传入的是 code 还是 ID
|
||
if form_id:
|
||
# 明确传了 form_id,code_or_id 就是 code
|
||
code = code_or_id
|
||
all_ids = [str(form_id)]
|
||
elif code_or_id and str(code_or_id).isdigit() and len(str(code_or_id)) > 15:
|
||
# 纯数字且长度>15,判定为 ID
|
||
all_ids = [str(code_or_id)]
|
||
else:
|
||
# 传的是 code,需要查找 ID
|
||
code = code_or_id
|
||
all_ids = []
|
||
|
||
# 优先从缓存获取
|
||
cached_id, _ = _cache_get(code)
|
||
if cached_id:
|
||
all_ids.append(cached_id)
|
||
|
||
# 再通过 queryByIdOrCode 快速查找
|
||
try:
|
||
r = api_request(f'/desform/queryByIdOrCode?desformCode={code}', method='GET')
|
||
if r.get('success') and r.get('result') and r['result'].get('id'):
|
||
qid = r['result']['id']
|
||
if qid not in all_ids:
|
||
all_ids.append(qid)
|
||
except Exception:
|
||
pass
|
||
|
||
# 如果快速查找没结果,再走 list 全量搜索兜底
|
||
if not all_ids:
|
||
page = 1
|
||
while page <= 10:
|
||
r = api_request(f'/desform/list?pageNo={page}&pageSize=100&column=createTime&order=desc', method='GET')
|
||
if not r.get('success') or not r.get('result'):
|
||
break
|
||
records = r['result'].get('records', [])
|
||
if not records:
|
||
break
|
||
for rec in records:
|
||
if rec.get('desformCode') == code:
|
||
all_ids.append(rec['id'])
|
||
total = r['result'].get('total', 0)
|
||
if page * 100 >= total:
|
||
break
|
||
page += 1
|
||
|
||
if not all_ids:
|
||
print(f' {code_or_id}: 未找到表单,无需删除')
|
||
return deleted_ids
|
||
|
||
for fid in all_ids:
|
||
try:
|
||
# Step 1: 逻辑删除
|
||
r2 = api_request(f'/desform/deleteBatch?ids={fid}', method='DELETE')
|
||
ok2 = r2.get('success', False)
|
||
# Step 2: 真实删除
|
||
r3 = api_request(f'/desform/recycleBin/deleteByIds?ids={fid}', method='DELETE')
|
||
ok3 = r3.get('success', False)
|
||
if ok2 and ok3:
|
||
deleted_ids.append(fid)
|
||
print(f' {code_or_id}: 已删除 {fid}')
|
||
else:
|
||
print(f' {code_or_id}: 删除 {fid} 部分失败 (deleteBatch={ok2}, recycleBin={ok3})')
|
||
except Exception as e:
|
||
print(f' {code_or_id}: 删除 {fid} 异常: {e}')
|
||
|
||
# 清除缓存
|
||
if code:
|
||
_cache_remove(code)
|
||
return deleted_ids
|
||
|
||
|
||
def update_form(code, widgets, title_index=0):
|
||
"""修改已有表单设计:查询 → 重新保存设计 → 返回 (form_id, title_model)
|
||
|
||
Args:
|
||
code: 表单编码
|
||
widgets: 新的控件列表(同 create_form 格式)
|
||
title_index: 标题字段在 widgets 中的索引
|
||
"""
|
||
# 查询表单(带缓存)
|
||
form_id, uc = get_form_id(code)
|
||
if not form_id:
|
||
raise RuntimeError(f'表单 {code} 不存在,无法更新')
|
||
|
||
# 解包 widgets
|
||
top_items = []
|
||
all_models = []
|
||
for item in widgets:
|
||
if isinstance(item, tuple):
|
||
top_items.append(item[0])
|
||
all_models.append((item[1], item[2]))
|
||
else:
|
||
top_items.append(item)
|
||
all_models.append((item.get('key', ''), item.get('model', '')))
|
||
|
||
title_model = all_models[title_index][1] if title_index < len(all_models) else all_models[0][1]
|
||
|
||
# 保存设计
|
||
save_design(form_id, code, top_items, title_model, uc)
|
||
# 更新缓存(updateCount 会被后端自动递增)
|
||
_cache_put(code, form_id, uc + 1)
|
||
print(f' {code}: 已更新 (ID={form_id})')
|
||
|
||
return form_id, title_model
|
||
|
||
|
||
def _is_half_suitable(widget_type):
|
||
"""判断控件是否适合半行布局(textarea/editor/markdown/file-upload/imgupload/sub-table-design 等宽控件不适合)"""
|
||
wide_types = {'textarea', 'editor', 'markdown', 'file-upload', 'imgupload',
|
||
'sub-table-design', 'divider', 'map', 'hand-sign', 'grid', 'tabs'}
|
||
return widget_type not in wide_types
|
||
|
||
|
||
def _get_widget_type(item):
|
||
"""从 widget tuple 或 dict 中获取控件类型"""
|
||
if isinstance(item, tuple):
|
||
w = item[0]
|
||
else:
|
||
w = item
|
||
# card 容器:检查内部控件
|
||
if w.get('type') == 'card' and w.get('list'):
|
||
return w['list'][0].get('type', '')
|
||
return w.get('type', '')
|
||
|
||
|
||
def _get_inner_widget(item):
|
||
"""从 card-wrapped tuple 中提取内部控件 dict"""
|
||
if isinstance(item, tuple):
|
||
w = item[0]
|
||
else:
|
||
w = item
|
||
if w.get('type') == 'card' and w.get('list') and len(w['list']) == 1:
|
||
return w['list'][0]
|
||
return None
|
||
|
||
|
||
def _set_autowidth(widget, width):
|
||
"""设置控件的 autoWidth"""
|
||
if 'options' in widget and isinstance(widget['options'], dict):
|
||
widget['options']['autoWidth'] = width
|
||
|
||
|
||
def _apply_half_layout(widgets):
|
||
"""将 widgets 列表中适合的控件两两配对为半行布局
|
||
|
||
规则:
|
||
- textarea/editor/markdown/file-upload/imgupload/sub-table-design/divider 等保持整行
|
||
- 其余控件两两配对到同一个 card 中,autoWidth 设为 50
|
||
- 奇数个适合半行的控件时,最后一个保持整行
|
||
|
||
Args:
|
||
widgets: [(card_dict, key, model), ...] 或 dict 混合列表
|
||
|
||
Returns:
|
||
(new_top_items, all_models) — 重组后的顶层控件列表和 model 列表
|
||
"""
|
||
top_items = []
|
||
all_models = []
|
||
half_buffer = None # 缓存一个待配对的半行控件
|
||
|
||
for item in widgets:
|
||
wtype = _get_widget_type(item)
|
||
inner = _get_inner_widget(item)
|
||
|
||
if inner and _is_half_suitable(wtype):
|
||
# 适合半行布局
|
||
_set_autowidth(inner, 50)
|
||
if isinstance(item, tuple):
|
||
key, model = item[1], item[2]
|
||
else:
|
||
key, model = item.get('key', ''), item.get('model', '')
|
||
|
||
if half_buffer is None:
|
||
# 缓存等配对
|
||
half_buffer = (inner, key, model)
|
||
else:
|
||
# 配对成功,合并到一个 card
|
||
paired_card = make_card(half_buffer[0], inner)
|
||
top_items.append(paired_card)
|
||
all_models.append((half_buffer[1], half_buffer[2]))
|
||
all_models.append((key, model))
|
||
half_buffer = None
|
||
else:
|
||
# 不适合半行的控件,先刷出缓冲区
|
||
if half_buffer is not None:
|
||
_set_autowidth(half_buffer[0], 100) # 恢复整行
|
||
solo_card = make_card(half_buffer[0])
|
||
top_items.append(solo_card)
|
||
all_models.append((half_buffer[1], half_buffer[2]))
|
||
half_buffer = None
|
||
|
||
# 原样添加
|
||
if isinstance(item, tuple):
|
||
top_items.append(item[0])
|
||
all_models.append((item[1], item[2]))
|
||
else:
|
||
top_items.append(item)
|
||
all_models.append((item.get('key', ''), item.get('model', '')))
|
||
|
||
# 刷出最后的缓冲区
|
||
if half_buffer is not None:
|
||
_set_autowidth(half_buffer[0], 100)
|
||
solo_card = make_card(half_buffer[0])
|
||
top_items.append(solo_card)
|
||
all_models.append((half_buffer[1], half_buffer[2]))
|
||
|
||
return top_items, all_models
|
||
|
||
|
||
def create_form(name, code, widgets, title_index=0, layout='auto'):
|
||
"""一站式创建表单:查找/创建 → 保存设计 → 返回 (form_id, title_model)
|
||
|
||
Args:
|
||
name: 表单名称
|
||
code: 表单编码
|
||
widgets: 顶层控件列表(card 包裹的 tuple 和裸控件 tuple 混合)
|
||
title_index: 标题字段在 widgets 中的索引
|
||
layout: 布局模式
|
||
- 'auto': 字段数 >= 6 时自动使用半行布局(默认)
|
||
- 'half': 强制半行布局
|
||
- 'full': 强制整行布局(不做半行处理)
|
||
- 'word': Word 风格布局(带边框表格样式)
|
||
|
||
Returns:
|
||
(form_id, title_model)
|
||
"""
|
||
form_style = 'word' if layout == 'word' else 'normal'
|
||
|
||
if layout == 'word':
|
||
top_items, all_models = _apply_word_layout(widgets, form_name=name)
|
||
elif layout == 'half' or (layout == 'auto' and len(widgets) >= 6):
|
||
top_items, all_models = _apply_half_layout(widgets)
|
||
else:
|
||
# 原有逻辑:逐个解包
|
||
top_items = []
|
||
all_models = []
|
||
for item in widgets:
|
||
if isinstance(item, tuple):
|
||
top_items.append(item[0])
|
||
all_models.append((item[1], item[2]))
|
||
else:
|
||
top_items.append(item)
|
||
all_models.append((item.get('key', ''), item.get('model', '')))
|
||
|
||
# 确定标题字段
|
||
title_model = all_models[title_index][1] if title_index < len(all_models) else all_models[0][1]
|
||
|
||
# 查找或创建
|
||
form_id, uc, actual_code = find_or_create_form(name, code)
|
||
print(f' ID={form_id}, success=True')
|
||
|
||
# 保存设计
|
||
save_design(form_id, actual_code, top_items, title_model, uc, form_style)
|
||
# 更新缓存
|
||
_cache_put(actual_code, form_id, uc + 1)
|
||
|
||
return form_id, title_model
|