Files
2026-09-02 11:44:52 +08:00

316 lines
9.6 KiB
Python

"""
宜搭表单字段构造 — 按 type 分发,每种字段类型产出一个 component dict。
支持 19 种字段类型 + Divider。被 yida_form_builder.py 调用。
"""
from __future__ import annotations
import json
from typing import Any, Optional
from yida_schema_common import (
generate_field_id,
i18n,
next_node_id,
normalize_field_type,
build_option_data_source,
OPTION_FIELD_TYPES,
SUPPORTED_FIELD_TYPES,
)
def build_field_component(
field: dict[str, Any],
*,
used_colors: Optional[set[str]] = None,
app_type: str = "",
form_uuid: str = "",
corp_id: str = "",
) -> tuple[dict[str, Any], set[str]]:
"""
根据字段定义 dict 构造一个组件节点。
返回 (component_node, updated_used_colors)。
"""
if used_colors is None:
used_colors = set()
raw_type = field.get("type", "")
component_name = normalize_field_type(raw_type)
label = field.get("label", "")
required = field.get("required", False)
field_id = generate_field_id(component_name)
node: dict[str, Any] = {
"componentName": component_name,
"id": next_node_id(),
"props": {
"label": i18n(label),
"fieldId": field_id,
"__category__": "form",
"behavior": field.get("behavior", "NORMAL"),
"visibility": ["PC", "MOBILE"],
"submittable": "DEFAULT",
},
}
props = node["props"]
# required
if required:
props["validation"] = [{"type": "required"}]
# placeholder
if field.get("placeholder"):
props["placeholder"] = i18n(field["placeholder"])
# defaultValue
if field.get("defaultValue") is not None:
props["defaultValue"] = field["defaultValue"]
# --- 按类型分发 ---
if component_name == "TextareaField":
_apply_textarea_props(props, field)
elif component_name == "NumberField":
_apply_number_props(props, field)
elif component_name == "RateField":
_apply_rate_props(props, field)
elif component_name in ("DateField", "CascadeDateField"):
_apply_date_props(props, field, component_name)
elif component_name in OPTION_FIELD_TYPES:
used_colors = _apply_option_props(props, field, component_name, used_colors)
elif component_name in ("EmployeeField", "DepartmentSelectField"):
_apply_people_props(props, field)
elif component_name == "CountrySelectField":
_apply_people_props(props, field)
elif component_name == "AddressField":
_apply_address_props(props, field)
elif component_name == "AttachmentField":
_apply_attachment_props(props, field)
elif component_name == "ImageField":
_apply_image_props(props, field)
elif component_name == "SerialNumberField":
_apply_serial_number_props(props, field, app_type, form_uuid, field_id, corp_id)
elif component_name == "TableField":
used_colors = _apply_table_props(node, field, used_colors, app_type, form_uuid, corp_id)
elif component_name == "AssociationFormField":
_apply_association_props(props, field, app_type)
elif component_name == "Divider":
_apply_divider_props(props, field, label)
return node, used_colors
# ---------------------------------------------------------------------------
# 各类型 props 应用
# ---------------------------------------------------------------------------
def _apply_textarea_props(props: dict, field: dict) -> None:
props["rows"] = field.get("rows", 4)
props["htmlType"] = "textarea"
def _apply_number_props(props: dict, field: dict) -> None:
fmt = field.get("format", "INT")
if fmt == "FLOAT":
props["precision"] = field.get("precision", 2)
props["format"] = "money_w4"
elif fmt == "PERCENT":
props["precision"] = field.get("precision", 2)
props["format"] = "percent"
else:
props["precision"] = 0
props["format"] = "integer"
if field.get("suffix"):
props["innerAfter"] = i18n(field["suffix"])
if field.get("prefix"):
props["innerBefore"] = i18n(field["prefix"])
if field.get("min") is not None:
props["min"] = field["min"]
if field.get("max") is not None:
props["max"] = field["max"]
def _apply_rate_props(props: dict, field: dict) -> None:
props["count"] = field.get("total", 5)
if field.get("allowHalf"):
props["allowHalf"] = True
def _apply_date_props(props: dict, field: dict, component_name: str) -> None:
fmt = field.get("format", "yyyy-MM-dd")
props["format"] = fmt
if "HH" in fmt:
props["showTime"] = True
def _apply_option_props(
props: dict,
field: dict,
component_name: str,
used_colors: set[str],
) -> set[str]:
options = field.get("options", [])
if not options:
return used_colors
is_checkbox = component_name in ("CheckboxField", "MultiSelectField")
props["isUseDataSourceColor"] = True
data_source, used_colors = build_option_data_source(
options, is_checkbox=is_checkbox, used_colors=used_colors
)
props["dataSource"] = data_source
if not is_checkbox and data_source:
props["value"] = data_source[0]["value"]
return used_colors
def _apply_people_props(props: dict, field: dict) -> None:
multi = field.get("multi") or field.get("multiple")
if multi:
props["multiple"] = True
props["mode"] = "multiple"
def _apply_address_props(props: dict, field: dict) -> None:
level = field.get("level", "ADDRESS")
props["addressType"] = level
def _apply_attachment_props(props: dict, field: dict) -> None:
props["autoUpload"] = True
props["maxFileSize"] = field.get("maxFileSize", 100)
if field.get("maxFiles"):
props["maxItems"] = field["maxFiles"]
if field.get("fileTypes"):
props["accept"] = field["fileTypes"]
def _apply_image_props(props: dict, field: dict) -> None:
props["autoUpload"] = True
if field.get("maxFiles"):
props["maxItems"] = field["maxFiles"]
def _apply_serial_number_props(
props: dict,
field: dict,
app_type: str,
form_uuid: str,
field_id: str,
corp_id: str,
) -> None:
# 流水号不允许 required
if "validation" in props:
props["validation"] = [v for v in props["validation"] if v.get("type") != "required"]
serial_rule = field.get("serialNumberRule") or [
{
"__hide_delete__": False,
"ruleType": "date",
"content": "",
"formField": "",
"dateFormat": "yyyyMMdd",
"timeZone": "+8",
"digitCount": 4,
"isFixed": True,
"isFixedTips": "",
"resetPeriod": "noClean",
"resetPeriodTips": "",
"initialValue": 1,
},
{
"__hide_delete__": True,
"ruleType": "autoCount",
"content": "",
"formField": "",
"dateFormat": "yyyyMMdd",
"timeZone": "+8",
"digitCount": "4",
"isFixed": True,
"isFixedTips": "",
"resetPeriod": "noClean",
"resetPeriodTips": "",
"initialValue": 1,
},
]
props["serialNumberRule"] = serial_rule
serial_rule_json = json.dumps({"type": "custom", "value": serial_rule}).replace('"', '\\"')
props["formula"] = {
"expression": f'SERIALNUMBER("{corp_id}", "{app_type}", "{form_uuid}", "{field_id}", "{serial_rule_json}")'
}
def _apply_table_props(
node: dict,
field: dict,
used_colors: set[str],
app_type: str,
form_uuid: str,
corp_id: str,
) -> set[str]:
props = node["props"]
props["layout"] = "TABLE"
props["mobileLayout"] = "TILED"
props["maxItems"] = field.get("maxItems", 50)
children_defs = field.get("children", [])
children_nodes: list[dict[str, Any]] = []
for child_field in children_defs:
child_node, used_colors = build_field_component(
child_field,
used_colors=used_colors,
app_type=app_type,
form_uuid=form_uuid,
corp_id=corp_id,
)
children_nodes.append(child_node)
node["children"] = children_nodes
return used_colors
def _apply_association_props(props: dict, field: dict, app_type: str) -> None:
source_app = field.get("sourceApp", app_type)
source_form = field.get("sourceForm", "")
display_field_code = field.get("displayFieldCode", "")
if source_form:
props["associationForm"] = {
"appType": source_app,
"formUuid": source_form,
"formType": "receipt",
"formTitle": "",
"mainFieldId": display_field_code,
"mainComponentName": "TextField",
"mainFieldLabel": "",
}
props["dataFilterRules"] = {"instanceFieldId": None}
filling_rules = field.get("dataFillingRules", [])
if filling_rules:
normalized: list[dict[str, Any]] = []
for rule in filling_rules:
src = rule.get("source", "")
tgt = rule.get("target", "")
normalized.append({
"source": src,
"sourceFieldId": src,
"sourceType": rule.get("sourceType", "form"),
"target": tgt,
"targetFieldId": tgt,
"targetType": rule.get("targetType", "form"),
})
props["dataFillingRules"] = {"mainRules": normalized}
def _apply_divider_props(props: dict, field: dict, label: str) -> None:
props["title"] = i18n(label)
props["type"] = field.get("dividerType", "multi-parallelograms-end")
props.pop("validation", None)
props["behavior"] = "NORMAL"
props.pop("__category__", None)