Files
qhmes/.trae/skills/jeecg-desform/references/desform-examples.md

33 KiB
Raw Permalink Blame History

常见表单模式示例

模式 A简单信息录入表单

场景: 员工信息登记(姓名、手机、邮箱、部门、备注)

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 和 modeltype 中的 - 转为 _"""
    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 基础上,字段构建部分替换为:

# 请假类型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

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

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 分离构建:

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/editupdateCount 传当前值
9. 输出结果

关键注意事项:

  • time.sleep(0.003) 确保每个控件的时间戳不同
  • config.hasWidgets 必须包含所有使用到的控件 type包括 card
  • config.titleField 必须指向一个实际存在的控件 model
  • 必填字段需要同时设置 options.required = Truerules = [{"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
  1. advancedSetting.defaultValue.customConfig 必须为 true
  2. allowViewallowEditallowAddallowSelect 必须全部设为 true4 个操作选项默认全部勾选)
  3. titleField 必须填源表的真实标题字段 model(如 input_xxx),不能留空
  4. showFields 建议填入源表中需要展示的字段 model 列表
  5. 跨表单关联时,必须先创建被引用的表单,然后查询获取其字段 model再构建引用方的 link-record
  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 字段(缺一不可):
    {
      "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 中查询基础表单字段的方法

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

"""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_designtime.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 中默认全部开启