# 常见表单模式示例 ## 模式 A:简单信息录入表单 **场景:** 员工信息登记(姓名、手机、邮箱、部门、备注) ```python import urllib.request import json import time import random API_BASE = 'https://boot3.jeecg.com/jeecgboot' TOKEN = 'your-jwt-token-here' def api_request(path, data=None, method='POST'): 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) return json.loads(resp.read().decode('utf-8')) def gen_ids(widget_type): """生成 key 和 model(type 中的 - 转为 _)""" ts = int(time.time() * 1000) rnd1 = random.randint(100000, 999999) rnd2 = random.randint(100000, 999999) rnd3 = random.randint(100000, 999999) key = f"{ts}_{rnd1}" safe_type = widget_type.replace('-', '_') model = f"{safe_type}_{ts}_{rnd2}" card_key = f"{ts + 1}_{rnd3}" card_model = f"card_{ts + 1}_{rnd3}" return key, model, card_key, card_model def make_card_widget(widget_type, name, class_name, icon, options, required=False, extra_fields=None): """创建一个带 card 容器的控件""" key, model, card_key, card_model = gen_ids(widget_type) time.sleep(0.002) # 确保时间戳不同 widget = { "type": widget_type, "name": name, "className": class_name, "icon": icon, "hideTitle": False, "options": options, "advancedSetting": { "defaultValue": { "type": "compose", "value": "", "format": "string", "allowFunc": True, "valueSplit": "", "customConfig": False } }, "remoteAPI": {"url": "", "executed": False}, "key": key, "model": model, "modelType": "main", "rules": [{"required": True, "message": "${title}必须填写"}] if required else [], "isSubItem": False } if extra_fields: widget.update(extra_fields) card = { "key": card_key, "type": "card", "isAutoGrid": True, "isContainer": True, "list": [widget], "options": {}, "model": card_model } return card, model # ---- 构建表单字段 ---- widgets = [] used_types = set(["card"]) title_model = None # 1. 姓名(input,必填,标题字段) card, model = make_card_widget("input", "姓名", "form-input", "icon-input", { "width": "100%", "defaultValue": "", "required": True, "dataType": None, "pattern": "", "patternMessage": "", "placeholder": "", "clearable": False, "readonly": False, "disabled": False, "fillRuleCode": "", "showPassword": False, "unique": False, "hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": 100 }, required=True) widgets.append(card) used_types.add("input") title_model = model # 2. 手机(phone) card, _ = make_card_widget("phone", "手机", "form-input-phone", "icon-mobile-phone", { "width": "300px", "defaultValue": "", "required": False, "placeholder": "", "readonly": False, "disabled": False, "unique": False, "hidden": False, "showVerifyCode": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": 100 }, extra_fields={ "defaultRules": [ {"type": "phone", "message": "请输入正确的手机号码"}, {"type": "validator", "message": "", "trigger": "blur"} ] }) widgets.append(card) used_types.add("phone") # 3. 邮箱(email) card, _ = make_card_widget("email", "邮箱", "form-input-email", "icon-email", { "width": "300px", "defaultValue": "", "required": False, "placeholder": "", "readonly": False, "disabled": False, "unique": False, "hidden": False, "showVerifyCode": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": 100 }, extra_fields={ "defaultRules": [ {"type": "email", "message": "请输入正确的邮箱地址"}, {"type": "validator", "message": "", "trigger": "blur"} ] }) widgets.append(card) used_types.add("email") # 4. 部门(select-depart) card, _ = make_card_widget("select-depart", "部门", "form-select-depart", "icon-depart", { "keyMaps": [], "defaultValue": "", "defaultLogin": False, "placeholder": "", "width": "100%", "multiple": False, "disabled": False, "customReturnField": "id", "hidden": False, "dataAuthType": "member", "hiddenOnAdd": False, "required": False, "fieldNote": "", "autoWidth": 100 }) widgets.append(card) used_types.add("select-depart") # 5. 备注(textarea) card, _ = make_card_widget("textarea", "备注", "form-textarea", "icon-textarea", { "width": "100%", "defaultValue": "", "required": False, "disabled": False, "pattern": "", "patternMessage": "", "placeholder": "", "readonly": False, "unique": False, "hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": 100 }) widgets.append(card) used_types.add("textarea") # ---- 构建完整 JSON ---- design_json = { "list": widgets, "config": { "formStyle": "normal", "titleField": title_model, "showHeaderTitle": True, "labelWidth": 100, "labelPosition": "top", "size": "small", "dialogOptions": { "top": 20, "width": 1000, "padding": {"top": 25, "right": 25, "bottom": 30, "left": 25} }, "disabledAutoGrid": False, "designMobileView": False, "enableComment": True, "hasWidgets": sorted(list(used_types)), "defaultLoadLargeControls": False, "expand": {"js": "", "css": "", "url": {"js": "", "css": ""}}, "transactional": True, "customRequestURL": [{"url": ""}], "disableMobileCss": True, "allowExternalLink": False, "externalLinkShowData": False, "headerImgUrl": "", "externalTitle": "", "enableNotice": False, "noticeMode": "external", "noticeType": "system", "noticeReceiver": "", "allowPrint": False, "allowJmReport": False, "jmReportURL": "", "bizRuleConfig": [], "bigDataMode": False } } # ---- 创建或查询表单 ---- form_name = "员工信息登记" form_code = "yuan_gong_xin_xi_deng_ji" # 先检查是否已存在(避免重复创建报错) try: query_result = api_request(f'/desform/queryByCode?desformCode={form_code}', method='GET') if query_result.get('success') and query_result.get('result'): form_id = query_result['result']['id'] update_count = query_result['result'].get('updateCount', 1) print(f'表单已存在,ID: {form_id},将更新设计...') else: raise Exception('not found') except: # 创建新表单(注意:add 返回的 result 为 null,不能直接取 ID) add_result = api_request('/desform/add', { 'desformName': form_name, 'desformCode': form_code }) if not add_result.get('success'): print('创建失败!', add_result) exit(1) # 必须通过 queryByCode 获取表单 ID query_result = api_request(f'/desform/queryByCode?desformCode={form_code}', method='GET') form_id = query_result['result']['id'] update_count = query_result['result'].get('updateCount', 1) print(f'表单ID: {form_id}') # ---- 保存设计(updateCount 传当前值,后端自动递增) ---- edit_result = api_request('/desform/edit', { 'id': form_id, 'desformDesignJson': json.dumps(design_json, ensure_ascii=False), 'updateCount': update_count, 'autoNumberDesignConfig': {'update': {}, 'current': {}}, 'refTableDefaultValDbSync': {'changes': {}, 'removeKeys': []} }, method='PUT') print('保存结果:', json.dumps(edit_result, ensure_ascii=False, indent=2)) if edit_result.get('success'): print(f'\n表单创建成功!') print(f'表单ID: {form_id}') print(f'表单名称: {form_name}') print(f'表单编码: {form_code}') else: print('保存失败!') ``` --- ## 模式 B:带选项的审批表单 **场景:** 请假申请(请假类型单选、日期范围、天数、原因、附件) 在模式 A 基础上,字段构建部分替换为: ```python # 请假类型(radio,必填) card, _ = make_card_widget("radio", "请假类型", "form-radio", "icon-radio-active", { "inline": True, "matrixWidth": 120, "defaultValue": "", "showType": "default", "showLabel": False, "useColor": False, "colorIteratorIndex": 3, "options": [ {"value": "事假", "itemColor": "#2196F3"}, {"value": "病假", "itemColor": "#08C9C9"}, {"value": "年假", "itemColor": "#00C345"}, {"value": "婚假", "itemColor": "#FF9800"} ], "required": True, "width": "", "remote": False, "remoteOptions": [], "props": {"value": "value", "label": "label"}, "remoteFunc": "", "disabled": False, "hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": 100 }, required=True) # radio 的 advancedSetting 需要特殊设置 card["list"][0]["advancedSetting"]["defaultValue"]["valueSplit"] = "," card["list"][0]["advancedSetting"]["defaultValue"]["customConfig"] = True widgets.append(card) used_types.add("radio") # 开始日期(date,必填) card, _ = make_card_widget("date", "开始日期", "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": True, "width": "", "hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": 50 # 半行 }, required=True) widgets.append(card) used_types.add("date") ``` --- ## 模式 C:半行布局(一行两字段) 一个 card 内放两个控件,每个 autoWidth 设为 50: ```python ts = int(time.time() * 1000) card = { "key": f"{ts + 1}_{random.randint(100000, 999999)}", "type": "card", "isAutoGrid": True, "isContainer": True, "list": [ { "type": "input", "name": "姓名", "className": "form-input", "icon": "icon-input", "hideTitle": False, "options": { "width": "100%", "defaultValue": "", "required": True, "dataType": None, "pattern": "", "patternMessage": "", "placeholder": "", "clearable": False, "readonly": False, "disabled": False, "fillRuleCode": "", "showPassword": False, "unique": False, "hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": 50 # 半行 }, "advancedSetting": {"defaultValue": {"type": "compose", "value": "", "format": "string", "allowFunc": True, "valueSplit": "", "customConfig": False}}, "remoteAPI": {"url": "", "executed": False}, "key": f"{ts}_{random.randint(100000, 999999)}", "model": f"input_{ts}_{random.randint(100000, 999999)}", "modelType": "main", "rules": [{"required": True, "message": "${title}必须填写"}], "isSubItem": False }, { "type": "phone", "name": "手机", "className": "form-input-phone", "icon": "icon-mobile-phone", "hideTitle": False, "options": { "width": "300px", "defaultValue": "", "required": False, "placeholder": "", "readonly": False, "disabled": False, "unique": False, "hidden": False, "showVerifyCode": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": 50 # 半行 }, "defaultRules": [ {"type": "phone", "message": "请输入正确的手机号码"}, {"type": "validator", "message": "", "trigger": "blur"} ], "advancedSetting": {"defaultValue": {"type": "compose", "value": "", "format": "string", "allowFunc": True, "valueSplit": "", "customConfig": False}}, "remoteAPI": {"url": "", "executed": False}, "key": f"{ts}_{random.randint(100000, 999999)}", "model": f"phone_{ts}_{random.randint(100000, 999999)}", "modelType": "main", "rules": [], "isSubItem": False } ], "options": {}, "model": f"card_{ts + 1}_{random.randint(100000, 999999)}" } ``` --- ## 模式 D:带子表的表单 **场景:** 采购单主表 + 采购明细子表 子表控件直接放在 list 顶层(不需要 card): ```python sub_table_key = f"{int(time.time() * 1000)}_{random.randint(100000, 999999)}" sub_table_model = f"sub_table_design_{int(time.time() * 1000)}_{random.randint(100000, 999999)}" sub_table = { "type": "sub-table-design", "name": "采购明细", "className": "form-sub-table", "icon": "icon-table", "hideTitle": False, "class": ["data-j-editable-design"], "isContainer": True, "columns": [ { "span": 12, "list": [ { "type": "input", "name": "物品名称", "className": "form-input", "icon": "icon-input", "hideTitle": False, "options": { "width": "100%", "defaultValue": "", "required": True, "dataType": None, "pattern": "", "patternMessage": "", "placeholder": "", "clearable": False, "readonly": False, "disabled": False, "fillRuleCode": "", "showPassword": False, "unique": False, "hidden": False, "hiddenOnAdd": False, "fieldNote": "" }, "advancedSetting": {"defaultValue": {"type": "compose", "value": "", "format": "string", "allowFunc": True, "valueSplit": "", "customConfig": False}}, "remoteAPI": {"url": "", "executed": False}, "key": f"{int(time.time() * 1000)}_{random.randint(100000, 999999)}", "model": f"input_{int(time.time() * 1000)}_{random.randint(100000, 999999)}", "modelType": "main", "rules": [{"required": True, "message": "${title}必须填写"}], "isSubItem": True, "subOptions": {"width": "200px", "parentKey": sub_table_key} }, { "type": "number", "name": "数量", "className": "form-number", "icon": "icon-number", "hideTitle": False, "options": { "width": "", "required": False, "defaultValue": 0, "placeholder": "", "controls": False, "min": 0, "minUnlimited": True, "max": 100, "maxUnlimited": True, "step": 1, "disabled": False, "controlsPosition": "right", "unitText": "", "unitPosition": "suffix", "showPercent": False, "align": "left", "hidden": False, "hiddenOnAdd": False, "fieldNote": "" }, "advancedSetting": {"defaultValue": {"type": "compose", "value": "", "format": "number", "allowFunc": True, "valueSplit": "", "customConfig": False}}, "remoteAPI": {"url": "", "executed": False}, "key": f"{int(time.time() * 1000)}_{random.randint(100000, 999999)}", "model": f"number_{int(time.time() * 1000)}_{random.randint(100000, 999999)}", "modelType": "main", "rules": [], "isSubItem": True, "subOptions": {"width": "200px", "parentKey": sub_table_key} } ] }, { "span": 12, "list": [ { "type": "money", "name": "单价", "className": "form-money", "icon": "icon-money", "hideTitle": False, "options": { "width": "180px", "placeholder": "请输入金额", "required": False, "unitText": "元", "unitPosition": "suffix", "precision": 2, "hidden": False, "disabled": False, "hiddenOnAdd": False, "fieldNote": "" }, "advancedSetting": {"defaultValue": {"type": "compose", "value": "", "format": "number", "allowFunc": True, "valueSplit": "", "customConfig": False}}, "remoteAPI": {"url": "", "executed": False}, "key": f"{int(time.time() * 1000)}_{random.randint(100000, 999999)}", "model": f"money_{int(time.time() * 1000)}_{random.randint(100000, 999999)}", "modelType": "main", "rules": [], "isSubItem": True, "subOptions": {"width": "200px", "parentKey": sub_table_key} } ] } ], "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": {"defaultValue": {"type": "compose", "value": "", "format": "string", "allowFunc": True, "valueSplit": "", "customConfig": True}}, "key": sub_table_key, "model": sub_table_model, "modelType": "main", "rules": [], "isSubItem": False } # 直接加入 list 顶层(不需要 card 容器) widgets.append(sub_table) used_types.add("sub-table-design") ``` **子表控件要点:** 1. 子控件 `isSubItem: True` 2. 子控件有 `subOptions: { "width": "200px", "parentKey": "子表的key" }` 3. 子控件的 options 没有 `autoWidth` 字段 4. `columns` 数组中每个元素有 `span`(栅格宽度)和 `list`(控件列表) --- ## 模式 E:多子表订单表单 **场景:** 订单表(主表 + 商品明细 + 收款记录 + 发货记录 三个子表) 在模式 A 基础上,使用 `make_widget` + `make_card` + `make_sub_table` 分离构建: ```python def make_widget(widget_type, name, class_name, icon, options, required=False, is_sub=False, parent_key=None, extra=None): """创建控件(支持主表和子表控件,type 中的 - 自动转 _)""" ts = int(time.time() * 1000) rnd1 = random.randint(100000, 999999) rnd2 = random.randint(100000, 999999) key = f"{ts}_{rnd1}" safe_type = widget_type.replace('-', '_') model = f"{safe_type}_{ts}_{rnd2}" time.sleep(0.003) fmt = "number" if widget_type in ("number", "integer", "money", "slider") else "string" custom = widget_type in ("radio", "checkbox", "select", "link-record", "sub-table-design") 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 } # link-field 不需要 advancedSetting(其他控件都需要) if widget_type != "link-field": w["advancedSetting"] = {"defaultValue": { "type": "compose", "value": "", "format": fmt, "allowFunc": True, "valueSplit": "," if custom else "", "customConfig": custom }} if is_sub and parent_key: w["subOptions"] = {"width": "200px", "parentKey": parent_key} if extra: w.update(extra) return w, key, model def make_card(*widgets_list): """创建 card 容器,支持放入 1~2 个控件""" ts = int(time.time() * 1000) rnd = random.randint(100000, 999999) time.sleep(0.003) return { "key": f"{ts}_{rnd}", "type": "card", "isAutoGrid": True, "isContainer": True, "list": list(widgets_list), "options": {}, "model": f"card_{ts}_{rnd}" } def make_sub_table(name, sub_widgets): """创建子表,sub_widgets 为子控件列表""" ts = int(time.time() * 1000) rnd1 = random.randint(100000, 999999) rnd2 = random.randint(100000, 999999) st_key = f"{ts}_{rnd1}" st_model = f"sub_table_design_{ts}_{rnd2}" time.sleep(0.003) 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": {"defaultValue": {"type": "compose", "value": "", "format": "string", "allowFunc": True, "valueSplit": "", "customConfig": True}}, "key": st_key, "model": st_model, "modelType": "main", "rules": [], "isSubItem": False }, st_key # 构建子表(先创建子表获取 key,再创建子控件绑定 parentKey) sub_widgets = [] # 先占位获取子表 key sub, sub_key = make_sub_table("商品明细", []) # 创建子控件 w, _, _ = make_widget("input", "商品名称", "form-input", "icon-input", {...}, required=True, is_sub=True, parent_key=sub_key) sub_widgets.append(w) w, _, _ = make_widget("integer", "数量", "form-integer", "icon-integer", {...}, required=True, is_sub=True, parent_key=sub_key) sub_widgets.append(w) # 更新子表的 columns sub["columns"] = [{"span": 24, "list": sub_widgets}] all_widgets.append(sub) ``` **多子表要点:** 1. 每个子表独立调用 `make_sub_table()` 获取 `st_key` 2. 子控件创建时传 `is_sub=True, parent_key=st_key` 3. 子控件的 options **没有** `autoWidth` 字段 4. 多个子表按顺序追加到 `all_widgets`(顶层 list) 5. `config.hasWidgets` 中只需加一次 `"sub-table-design"` --- ## 执行模板(完整脚本框架) 生成脚本时遵循此框架: ``` 1. 导入依赖 (urllib, json, time, random) 2. 配置 API_BASE 和 TOKEN 3. 定义工具函数 (api_request, make_widget, make_card, make_sub_table) 4. 构建各字段控件(主表 + 子表) 5. 组装 design_json (list + config) 6. 检查表单是否已存在(GET /desform/queryByCode) 7. 不存在则创建(POST /desform/add),再查询获取 ID 8. 保存设计(PUT /desform/edit),updateCount 传当前值 9. 输出结果 ``` **关键注意事项:** - `time.sleep(0.003)` 确保每个控件的时间戳不同 - `config.hasWidgets` 必须包含所有使用到的控件 type(包括 `card`) - `config.titleField` 必须指向一个实际存在的控件 model - 必填字段需要同时设置 `options.required = True` 和 `rules = [{"required": True, ...}]` - 数字类控件的 advancedSetting.defaultValue.format 应为 `"number"` - **`POST /desform/add` 返回 `result: null`**,必须用 `GET /desform/queryByCode` 获取 ID - **`updateCount` 传当前数据库值**(不是 +1),后端自动递增 - **先检查表单是否存在**,避免重复创建报 `该code已存在` 错误 --- ## 实战踩坑清单(className / icon 易错汇总) 生成表单时最容易出错的是 className 和 icon,以下为**实测验证的正确值**: | 控件 type | className | icon | 特殊说明 | |-----------|-----------|------|----------| | `link-record` | `form-link-record` | **`icon-link`** | 不是 `icon-link-record`! | | `link-field` | `form-link-field` | **`icon-field`** | 不是 `icon-link-field`! | | `sub-table-design` | **`form-sub-table`** | **`icon-table`** | 不是 `form-sub-table-design` / `icon-sub-table-design`! | | `divider` | `form-divider` | `icon-fengexian` | | ### link-record 踩坑要点 1. **`advancedSetting.defaultValue.customConfig` 必须为 `true`** 2. **`allowView`、`allowEdit`、`allowAdd`、`allowSelect` 必须全部设为 `true`**(4 个操作选项默认全部勾选) 3. **`titleField` 必须填源表的真实标题字段 model**(如 `input_xxx`),不能留空 4. **`showFields` 建议填入源表中需要展示的字段 model 列表** 5. **跨表单关联时**,必须先创建被引用的表单,然后查询获取其字段 model,再构建引用方的 link-record ### link-field 踩坑要点 1. **link-field 没有 `advancedSetting`** — 与其他控件不同 2. **`linkRecordKey` 填的是 link-record 的 key**(如 `1773457559119_461003`),**不是 model** 3. **`fieldType` 必须填来源字段的真实控件类型**(如 `"input"`, `"select-user"`, `"money"`),不能一律写 `"input"` 4. **`fieldOptions` 需包含源字段类型相关的 options 子集**,例如: - select-user: `{"multiple": false, "customReturnField": "username"}` - select (多选): `{"multiple": true}` - 普通 input/money 等: `{}` 即可 ### sub-table-design 踩坑要点 1. **options 必须包含 `allowAdd: true`**,否则子表没有"添加"按钮 2. **完整 options 字段(缺一不可):** ```json { "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": "" } ``` 3. **子表内可以放 link-record + link-field**,实现子表行级关联选择 ### 跨表单关联的正确流程(多表单批量创建) ``` 1. 先创建基础表单(如商品表、仓库表、供应商表) 2. 查询基础表单的 designJson,提取字段 model 和 titleField 3. 构建业务表单(如入库单)时: a. link-record.options.sourceCode = 基础表单的 desformCode b. link-record.options.titleField = 基础表单的 titleField model c. link-record.options.showFields = [基础表单中需要展示的字段 model 列表] d. link-field.options.linkRecordKey = 同表单中 link-record 控件的 KEY e. link-field.options.showField = 基础表单中要自动填充的字段 MODEL f. link-field.options.fieldType = 该字段的实际控件类型 ``` ### Python 中查询基础表单字段的方法 ```python def get_form_fields(form_code): """查询表单设计JSON,提取字段 model/key/type""" # 注意:queryByCode 不可靠,推荐用 queryByIdOrCode 或 desform_utils.py 中的 get_form_fields q = api_request(f'/desform/queryByIdOrCode?desformCode={form_code}', method='GET') 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 'model' in item and item.get('type') != 'card': fields[item['name']] = { 'model': item['model'], 'key': item['key'], 'type': item['type'] } extract(design.get('list', [])) return title_field, fields ``` ## 模式 F:使用 desform_utils.py 的跨表单 CRM 系统(推荐) **场景:** CRM 客户管理系统(4 个关联表单),使用共通工具库 `desform_utils.py` > **命名规则:** 模块名作为前缀,如 `crm_customer`(不是 `customer_crm`) ```python """CRM系统 - 4个关联表单""" import sys, time sys.path.insert(0, r'{后端项目根目录}') from desform_utils import * init_api('https://boot3.jeecg.com/jeecgboot', 'your-token') # ---- 1/4 客户信息(基础表单,被其他表单引用)---- fid1, uc1 = find_or_create_form('客户信息', 'crm_customer') w0, _, m0 = INPUT('客户名称', required=True) w1, _, _ = AUTONUMBER('客户编号', prefix='CRM') w2, _, _ = SELECT('客户类型', ['企业客户', '个人客户'], required=True) w3, _, _ = SELECT('所属行业', ['IT互联网', '金融', '制造业', '教育', '医疗', '房地产', '零售', '其他']) w4, _, _ = SELECT('客户来源', ['官网', '转介绍', '电话营销', '展会', '广告', '社交媒体', '其他']) w5, _, _ = RADIO('客户等级', ['A-重要', 'B-普通', 'C-一般']) w6, _, _ = RADIO('客户状态', ['潜在客户', '意向客户', '成交客户', '流失客户']) w7, _, _ = PHONE('联系电话') w8, _, _ = EMAIL('邮箱') w9, _, _ = AREA('所在地区') w10, _, _ = INPUT('详细地址') w11, _, _ = USER('负责销售', required=True) w12, _, _ = DEPART('所属部门') w13, _, _ = TEXTAREA('备注') save_design(fid1, 'crm_customer', [w0,w1,w2,w3,w4,w5,w6,w7,w8,w9,w10,w11,w12,w13], m0, uc1) # 查询客户表单字段,供后续表单关联引用 time.sleep(1) cust_tf, cust_fields = get_form_fields('crm_customer') cust_show = [cust_fields['客户编号']['model'], cust_fields['联系电话']['model']] # ---- 2/4 联系人(关联客户)---- fid2, uc2 = find_or_create_form('联系人', 'crm_contact') w0, _, m0 = INPUT('联系人姓名', required=True) w1, _, _ = LINK_RECORD('所属客户', 'crm_customer', cust_tf, cust_show, required=True) lr_key = w1['list'][0]['key'] # 获取 link-record 的 key,供 link-field 引用 w2, _, _ = LINK_FIELD('客户编号', lr_key, cust_fields['客户编号']['model'], field_type='auto-number') w3, _, _ = INPUT('职务') w4, _, _ = INPUT('部门') w5, _, _ = PHONE('手机号码', required=True) w6, _, _ = EMAIL('邮箱') w7, _, _ = SWITCH('是否决策人') w8, _, _ = TEXTAREA('备注') save_design(fid2, 'crm_contact', [w0,w1,w2,w3,w4,w5,w6,w7,w8], m0, uc2) # ---- 3/4 商机管理(关联客户 + 分隔符 + 金额 + 滑块)---- fid3, uc3 = find_or_create_form('商机管理', 'crm_opportunity') w0, _, m0 = INPUT('商机名称', required=True) w1, _, _ = AUTONUMBER('商机编号', prefix='BIZ') w2, _, _ = LINK_RECORD('关联客户', 'crm_customer', cust_tf, cust_show, required=True) lr_key = w2['list'][0]['key'] w3, _, _ = LINK_FIELD('客户编号', lr_key, cust_fields['客户编号']['model'], field_type='auto-number') w4, _, _ = DIVIDER('商机详情') w5, _, _ = SELECT('商机阶段', ['初步接触', '需求确认', '方案报价', '商务谈判', '赢单', '输单'], required=True) w6, _, _ = MONEY('预计金额', required=True) w7, _, _ = DATE('预计成交日期') w8, _, _ = USER('负责人', required=True) w9, _, _ = USER('协作人', multiple=True) w10, _, _ = SLIDER('赢单概率', show_input=True) w11, _, _ = TEXTAREA('备注') save_design(fid3, 'crm_opportunity', [w0,w1,w2,w3,w4,w5,w6,w7,w8,w9,w10,w11], m0, uc3) # ---- 4/4 跟进记录(关联客户 + 默认当前用户)---- fid4, uc4 = find_or_create_form('跟进记录', 'crm_follow_up') w0, _, m0 = INPUT('跟进主题', required=True) w1, _, _ = LINK_RECORD('关联客户', 'crm_customer', cust_tf, cust_show, required=True) lr_key = w1['list'][0]['key'] w2, _, _ = LINK_FIELD('客户名称', lr_key, cust_fields['客户名称']['model'], field_type='input') w3, _, _ = RADIO('跟进方式', ['电话', '拜访', '微信', '邮件', '会议', '其他'], required=True) w4, _, _ = DATE('跟进日期', required=True) w5, _, _ = DATE('下次跟进日期') w6, _, _ = USER('跟进人', required=True, default_login=True) w7, _, _ = TEXTAREA('跟进内容', required=True) w8, _, _ = FILE('附件') save_design(fid4, 'crm_follow_up', [w0,w1,w2,w3,w4,w5,w6,w7,w8], m0, uc4) # ---- 输出菜单 + 角色授权 SQL ---- print(gen_menu_sql('crm_menu', 'CRM客户管理', [ ('crm_customer_menu', '客户信息', 'crm_customer', 1), ('crm_contact_menu', '联系人', 'crm_contact', 2), ('crm_opportunity_menu', '商机管理', 'crm_opportunity', 3), ('crm_follow_up_menu', '跟进记录', 'crm_follow_up', 4), ])) ``` **跨表单关联要点:** 1. 先创建被引用的基础表单 → `save_design` → `time.sleep(1)` → `get_form_fields` 获取字段信息 2. `LINK_RECORD` 需要源表编码、titleField、showFields 3. `LINK_FIELD` 需要 link-record 的 **key**(通过 `w['list'][0]['key']` 获取)和源字段的 model + field_type 4. link-record 的 4 个操作选项(allowView/allowEdit/allowAdd/allowSelect)已在 desform_utils.py 中默认全部开启