Files
qhmes/.trae/skills/jeecg-desform/scripts/desform_utils.py

1625 lines
64 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 → 成功则查找 IDadd 失败(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` 控件居中、16pxflex 垂直居中
- 控件列的控件 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
}
# 标签列 optionsflex 垂直居中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_idcode_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