first commit

This commit is contained in:
2026-09-02 11:44:52 +08:00
commit 0c8fa2653e
309 changed files with 57278 additions and 0 deletions
@@ -0,0 +1,526 @@
"""
宜搭表单 schema 构造 — 字段组件装配进 Page > FormContainer 骨架。
主入口:
build_form_schema(form_title, fields, form_uuid, corp_id, app_type)
apply_changes_to_schema(schema, changes) — 增量操作
"""
from __future__ import annotations
import copy
import json
import re
from typing import Any, Optional
from yida_schema_common import (
build_components_map,
collect_component_names,
DATA_SOURCE_FIT_COMPILED,
DATA_SOURCE_FIT_SOURCE,
generate_field_id,
i18n,
next_node_id,
normalize_field_type,
UTILS_LEGAO_BUILTIN,
UTILS_YIDA_PLUGIN,
)
from yida_form_fields import build_field_component
# ---------------------------------------------------------------------------
# 骨架常量
# ---------------------------------------------------------------------------
_FORM_ACTIONS = {
"module": {
"source": (
'/**\n* 尊敬的用户,你好:页面 JS 面板是高阶用法。\n*/\n\n'
'export function didMount() {\n'
' console.log(`「页面 JS」:当前页面地址 ${location.href}`);\n'
'}'
),
"compiled": (
'"use strict";\n\nexports.__esModule = true;\nexports.didMount = didMount;\n'
'function didMount() {\n'
' console.log("\\u300C\\u9875\\u9762 JS\\u300D\\uFF1A\\u5F53\\u524D\\u9875\\u9762\\u5730\\u5740 " + location.href);\n'
'}\n'
),
},
"type": "FUNCTION",
"list": [{"id": "didMount", "title": "didMount"}],
}
_CONSTRUCTOR_SOURCE = (
"function constructor() {\n"
"var module = { exports: {} };\n"
"var _this = this;\n"
"this.__initMethods__(module.exports, module);\n"
"Object.keys(module.exports).forEach(function(item) {\n"
" if(typeof module.exports[item] === 'function'){\n"
" _this[item] = module.exports[item];\n"
" }\n"
"});\n\n"
"}"
)
# ---------------------------------------------------------------------------
# 主入口
# ---------------------------------------------------------------------------
def build_form_schema(
form_title: str,
fields: list[dict[str, Any]],
form_uuid: str = "",
corp_id: str = "",
app_type: str = "",
*,
label_align: str = "top",
) -> dict[str, Any]:
"""
构造完整的表单 schema。
Args:
form_title: 表单标题
fields: 字段定义数组(每项含 type + label + ...
form_uuid: 表单 UUID
corp_id: 企业 ID(流水号需要)
app_type: 应用编码
label_align: 标签对齐方式 top/left
"""
# 1. 构造所有字段节点
field_nodes: list[dict[str, Any]] = []
used_colors: set[str] = set()
for field in fields:
node, used_colors = build_field_component(
field,
used_colors=used_colors,
app_type=app_type,
form_uuid=form_uuid,
corp_id=corp_id,
)
field_nodes.append(node)
# 2. 后处理:解析 @label: 引用
_resolve_field_id_references(field_nodes)
# 3. 后处理:补全流水号 formula(此时 corp_id/app_type/form_uuid 确定)
_fill_serial_number_formulas(field_nodes, corp_id, app_type, form_uuid)
# 4. 收集 componentsMap
all_component_names = ["Page", "RootHeader", "RootContent", "RootFooter", "FormContainer"]
all_component_names.extend(collect_component_names(field_nodes))
components_map = build_components_map(all_component_names)
# 5. 拼装骨架
schema: dict[str, Any] = {
"schemaType": "superform",
"schemaVersion": "5.0",
"pages": [
{
"utils": [UTILS_LEGAO_BUILTIN, UTILS_YIDA_PLUGIN],
"componentsMap": components_map,
"componentsTree": [
{
"componentName": "Page",
"id": next_node_id(),
"props": {
"templateVersion": "1.0.0",
"pageStyle": {"backgroundColor": "#f2f3f5"},
"titleName": i18n(form_title),
"titleDesc": i18n(""),
"titleColor": "light",
"titleBg": "https://img.alicdn.com/imgextra/i2/O1CN0143ATPP1wIa9TrVvzN_!!6000000006285-2-tps-3360-400.png_.webp",
"backgroundColorCustom": "#f1f2f3",
"sizePc": "medium",
"labelAlignPc": label_align,
"labelWidthPc": "130px",
"labelWeightPc": "normal",
"contentMargin": "12",
"contentPadding": "20",
"contentBgColor": "white",
"showTitle": True,
"labelAlignMobile": "left",
"labelWidthMobile": "100px",
"labelWeightMobile": "bold",
"contentMarginMobile": "12",
"contentPaddingMobile": "0",
"contentBgColorMobile": "white",
"className": "page_m8o991i5",
},
"dataSource": {
"offline": [],
"globalConfig": {
"fit": {
"compiled": DATA_SOURCE_FIT_COMPILED,
"source": DATA_SOURCE_FIT_SOURCE,
"type": "js",
"error": {},
},
},
"online": [],
"list": [],
"sync": True,
},
"methods": {
"__initMethods__": {
"type": "js",
"source": "function (exports, module) { /*set actions code here*/ }",
"compiled": "function (exports, module) { /*set actions code here*/ }",
},
},
"lifeCycles": {
"componentDidMount": {
"id": "didMount",
"name": "didMount",
"params": {},
"type": "actionRef",
},
"componentWillUnmount": "",
"constructor": {
"type": "js",
"compiled": _CONSTRUCTOR_SOURCE,
"source": _CONSTRUCTOR_SOURCE,
},
},
"hidden": False,
"title": "",
"isLocked": False,
"condition": True,
"conditionGroup": "",
"children": [
{
"componentName": "RootHeader",
"id": next_node_id(),
"props": {},
"hidden": False,
"title": "",
"isLocked": False,
"condition": True,
"conditionGroup": "",
},
{
"componentName": "RootContent",
"id": next_node_id(),
"props": {},
"hidden": False,
"title": "",
"isLocked": False,
"condition": True,
"conditionGroup": "",
"children": [
{
"componentName": "FormContainer",
"id": next_node_id(),
"props": {
"columns": 1,
"labelAlign": label_align,
"submitText": i18n("提交", "Submit"),
"fieldId": generate_field_id("formContainer"),
"aiFormConfig": {
"systemPrompt": "",
"model": "qwen",
},
"beforeSubmit": False,
"afterSubmit": False,
},
"hidden": False,
"title": "",
"isLocked": False,
"condition": True,
"conditionGroup": "",
"children": field_nodes,
},
],
},
{
"componentName": "RootFooter",
"id": next_node_id(),
"props": {},
"hidden": False,
"title": "",
"isLocked": False,
"condition": True,
"conditionGroup": "",
},
],
"css": "body{background-color:#f2f3f5}",
},
],
"componentAlias": {"items": []},
"id": form_uuid or "xxxx",
"connectComponent": [],
},
],
"actions": copy.deepcopy(_FORM_ACTIONS),
"config": {"connectComponent": []},
}
return schema
# ---------------------------------------------------------------------------
# 增量修改
# ---------------------------------------------------------------------------
def apply_changes_to_schema(
schema: dict[str, Any],
changes: list[dict[str, Any]],
*,
corp_id: str = "",
app_type: str = "",
form_uuid: str = "",
) -> dict[str, Any]:
"""
在已有 schema 上执行增量 changesadd/update/delete)。
返回修改后的 schema(原地修改)。
"""
form_container = _find_form_container(schema)
if form_container is None:
raise ValueError("schema 中找不到 FormContainer 节点")
children: list[dict[str, Any]] = form_container.get("children", [])
used_colors = _collect_existing_colors(children)
for change in changes:
action = change.get("action", "")
if action == "add":
field_def = change.get("field", {})
node, used_colors = build_field_component(
field_def,
used_colors=used_colors,
app_type=app_type,
form_uuid=form_uuid,
corp_id=corp_id,
)
after_label = change.get("after")
before_label = change.get("before")
insert_idx = len(children)
if after_label:
idx = _find_field_index_by_label(children, after_label)
if idx is not None:
insert_idx = idx + 1
elif before_label:
idx = _find_field_index_by_label(children, before_label)
if idx is not None:
insert_idx = idx
children.insert(insert_idx, node)
elif action == "update":
label = change.get("label", "")
table_label = change.get("tableLabel")
patches = change.get("changes", {})
target_list = children
if table_label:
table_node = _find_field_by_label(children, table_label)
if table_node and table_node.get("componentName") == "TableField":
target_list = table_node.get("children", [])
target = _find_field_by_label(target_list, label)
if target:
_apply_field_patches(target, patches, used_colors)
elif action == "delete":
label = change.get("label", "")
table_label = change.get("tableLabel")
target_list = children
if table_label:
table_node = _find_field_by_label(children, table_label)
if table_node and table_node.get("componentName") == "TableField":
target_list = table_node.get("children", [])
idx = _find_field_index_by_label(target_list, label)
if idx is not None:
target_list.pop(idx)
# 后处理
all_fields = form_container.get("children", [])
_resolve_field_id_references(all_fields)
_fill_serial_number_formulas(all_fields, corp_id, app_type, form_uuid)
# 更新 componentsMap
all_names = ["Page", "RootHeader", "RootContent", "RootFooter", "FormContainer"]
all_names.extend(collect_component_names(all_fields))
schema["pages"][0]["componentsMap"] = build_components_map(all_names)
return schema
# ---------------------------------------------------------------------------
# 后处理
# ---------------------------------------------------------------------------
def _resolve_field_id_references(field_nodes: list[dict[str, Any]]) -> None:
"""解析 @label:字段名 引用为真实 fieldId。"""
label_to_field_id: dict[str, str] = {}
def _collect(nodes: list[dict[str, Any]]) -> None:
for node in nodes:
props = node.get("props", {})
label_obj = props.get("label", {})
label_text = label_obj.get("zh_CN", "") if isinstance(label_obj, dict) else str(label_obj)
field_id = props.get("fieldId", "")
if label_text and field_id:
label_to_field_id[label_text] = field_id
for child in node.get("children", []):
_collect([child])
_collect(field_nodes)
def _resolve(nodes: list[dict[str, Any]]) -> None:
for node in nodes:
props = node.get("props", {})
filling_rules = props.get("dataFillingRules", {})
if isinstance(filling_rules, dict):
main_rules = filling_rules.get("mainRules", [])
for rule in main_rules:
for key in ("source", "sourceFieldId", "target", "targetFieldId"):
val = rule.get(key, "")
if isinstance(val, str) and val.startswith("@label:"):
name = val[7:]
if name in label_to_field_id:
rule[key] = label_to_field_id[name]
for child in node.get("children", []):
_resolve([child])
_resolve(field_nodes)
def _fill_serial_number_formulas(
field_nodes: list[dict[str, Any]],
corp_id: str,
app_type: str,
form_uuid: str,
) -> None:
"""确保 SerialNumberField 的 formula 包含正确的 corp_id/app_type/form_uuid。"""
if not corp_id or not app_type or not form_uuid:
return
def _walk(nodes: list[dict[str, Any]]) -> None:
for node in nodes:
if node.get("componentName") == "SerialNumberField":
props = node.get("props", {})
field_id = props.get("fieldId", "")
serial_rule = props.get("serialNumberRule", [])
if serial_rule and field_id:
rule_json = json.dumps({"type": "custom", "value": serial_rule}).replace('"', '\\"')
props["formula"] = {
"expression": f'SERIALNUMBER("{corp_id}", "{app_type}", "{form_uuid}", "{field_id}", "{rule_json}")'
}
for child in node.get("children", []):
_walk([child])
_walk(field_nodes)
# ---------------------------------------------------------------------------
# 辅助函数
# ---------------------------------------------------------------------------
def _find_form_container(schema: dict[str, Any]) -> Optional[dict[str, Any]]:
pages = schema.get("pages", [])
if not pages:
return None
tree = pages[0].get("componentsTree", [])
if not tree:
return None
def _search(node: dict[str, Any]) -> Optional[dict[str, Any]]:
if node.get("componentName") == "FormContainer":
return node
for child in node.get("children", []):
found = _search(child)
if found:
return found
return None
return _search(tree[0])
def _find_field_by_label(nodes: list[dict[str, Any]], label: str) -> Optional[dict[str, Any]]:
for node in nodes:
props = node.get("props", {})
label_obj = props.get("label", {})
label_text = label_obj.get("zh_CN", "") if isinstance(label_obj, dict) else str(label_obj)
if label_text == label:
return node
return None
def _find_field_index_by_label(nodes: list[dict[str, Any]], label: str) -> Optional[int]:
for idx, node in enumerate(nodes):
props = node.get("props", {})
label_obj = props.get("label", {})
label_text = label_obj.get("zh_CN", "") if isinstance(label_obj, dict) else str(label_obj)
if label_text == label:
return idx
return None
def _collect_existing_colors(nodes: list[dict[str, Any]]) -> set[str]:
colors: set[str] = set()
for node in nodes:
props = node.get("props", {})
for item in props.get("dataSource", []):
c = item.get("color")
if c:
colors.add(c)
for child in node.get("children", []):
colors.update(_collect_existing_colors([child]))
return colors
def _apply_field_patches(node: dict[str, Any], patches: dict[str, Any], used_colors: set[str]) -> None:
props = node.get("props", {})
if "label" in patches:
props["label"] = i18n(patches["label"])
if "required" in patches:
validation = props.get("validation", [])
has_req = any(v.get("type") == "required" for v in validation)
if patches["required"] and not has_req:
validation.append({"type": "required"})
props["validation"] = validation
elif not patches["required"] and has_req:
props["validation"] = [v for v in validation if v.get("type") != "required"]
if "behavior" in patches:
props["behavior"] = patches["behavior"]
if "options" in patches:
from yida_schema_common import build_option_data_source
component_name = node.get("componentName", "")
is_checkbox = component_name in ("CheckboxField", "MultiSelectField")
ds, _ = build_option_data_source(patches["options"], is_checkbox=is_checkbox, used_colors=used_colors)
props["dataSource"] = ds
props["isUseDataSourceColor"] = True
if "placeholder" in patches:
props["placeholder"] = i18n(patches["placeholder"])
if "suffix" in patches:
props["innerAfter"] = i18n(patches["suffix"])
if "prefix" in patches:
props["innerBefore"] = i18n(patches["prefix"])
if "format" in patches:
props["format"] = patches["format"]
if "multiple" in patches or "multi" in patches:
val = patches.get("multiple") or patches.get("multi")
props["multiple"] = val
if val:
props["mode"] = "multiple"
def is_empty_skeleton(schema: dict[str, Any]) -> bool:
"""判断 schema 是否是空骨架(无字段)。"""
fc = _find_form_container(schema)
if fc is None:
return True
children = fc.get("children", [])
return len(children) == 0