""" 宜搭表单字段构造 — 按 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)