新增JeecgBoot BPM流程自动生成器,包含流程创建、修改及审批人配置功能,支持自然语言描述转化为BPMN XML,并通过API与JeecgBoot系统交互。

This commit is contained in:
geht
2026-04-08 16:24:41 +08:00
parent 7c60acd679
commit 67104af7de
168 changed files with 207167 additions and 8 deletions

View File

@@ -0,0 +1,754 @@
---
name: jeecg-desform
description: "Use when user asks to create/generate a form using AI, design a form automatically, or says \"AI设计表单\", \"AI生成表单\", \"自动创建表单\", \"智能表单\", \"生成一个表单\", \"帮我设计表单\", \"创建表单\", \"新建表单\", \"做一个表单\", \"ai form\", \"generate form\", \"create form\", \"design form\". Also triggers when user describes form fields like \"需要姓名、手机号、地址字段\" or mentions form requirements like \"做一个请假表单包含请假天数和原因\". Supports generating forms from screenshots — when user provides a screenshot/image of a form and asks to reproduce it (e.g., \"按照截图生成表单\", \"照着这个图片做表单\", \"根据截图创建表单\", \"generate form from screenshot\", \"recreate this form\")."
---
# JeecgBoot 表单设计器 AI 自动生成器
将自然语言的表单需求描述转换为 desformDesignJson并通过 API 在 JeecgBoot 系统中自动创建表单。
> **重要:本 skill 只处理「设计器表单」desform不涉及 Online 表单。两者是完全独立的表单体系。**
## 前置条件
用户必须提供以下信息(或由 AI 引导确认):
1. **API 地址**JeecgBoot 后端地址(如 `https://boot3.jeecg.com/jeecgboot`
2. **X-Access-Token**JWT 登录令牌(从浏览器 F12 获取)
如果用户未提供,提示:
> 请提供 JeecgBoot 后端地址和 X-Access-Token从浏览器 F12 → Network → 任意请求的 Request Headers 中复制)。
## 交互流程
### Step 0: 解析用户需求
从用户描述中提取以下信息:
| 信息 | 默认值 | 示例 |
|------|--------|------|
| 表单名称 | 用户指定或自动生成 | "员工请假申请" |
| 表单编码 | 英文命名,模块名前缀 | `oa_leave_apply`(不用拼音) |
| 字段列表 | 从描述中解析 | 姓名(必填)、请假天数(数字)、请假原因(多行文本) |
| 字段属性 | 从描述中推断 | 必填、默认值、选项列表等 |
### Step 1: 识别字段并选择控件类型
**控件类型映射规则:**
| 用户描述关键词 | 控件 type | 说明 |
|---------------|-----------|------|
| 名称/标题/姓名/文本 | `input` | 单行文本 |
| 描述/备注/原因/详情/多行 | `textarea` | 多行文本 |
| 数量/数字/金额(无单位) | `number` | 数字输入 |
| 整数/个数/天数 | `integer` | 整数输入 |
| 金额/费用/价格 | `money` | 金额(带元单位) |
| 单选/性别/是否/状态 | `radio` | 单选框组 |
| 多选/标签/兴趣 | `checkbox` | 多选框组 |
| 下拉/选择/类型/类别 | `select` | 下拉选择框 |
| 日期/生日/入职日期 | `date` | 日期选择器 |
| 时间/几点 | `time` | 时间选择器 |
| 开关/启用/是否激活 | `switch` | 开关 |
| 评分/星级/打分 | `rate` | 评分 |
| 颜色 | `color` | 颜色选择器 |
| 滑块/进度/百分比 | `slider` | 滑块 |
| 手机/电话/手机号 | `phone` | 手机 |
| 邮箱/Email | `email` | 邮箱 |
| 图片/照片/头像 | `imgupload` | 图片上传 |
| 附件/文件/上传 | `file-upload` | 文件上传 |
| 富文本/HTML内容 | `editor` | 富文本编辑器 |
| Markdown | `markdown` | Markdown 编辑器 |
| 省市/地区/地址选择 | `area-linkage` | 省市级联动 |
| 地图/位置(地图) | `map` | 地图 |
| 定位/GPS | `location` | 定位 |
| 条码/二维码 | `barcode` | 条码 |
| 自动编号/流水号 | `auto-number` | 自动编号 |
| 选人/审批人/负责人 | `select-user` | 用户组件 |
| 部门/选部门 | `select-depart` | 部门组件 |
| 岗位/选岗位 | `select-depart-post` | 岗位组件 |
| 分类树/树选择 | `select-tree` | 下拉树 |
| 表字典/弹窗选择 | `table-dict` | 表字典popup或模糊查询 |
| 关联记录/引用 | `link-record` | 关联其他表单的记录 |
| 他表字段/自动填充 | `link-field` | 显示关联记录的字段值 |
| 公式/自动计算 | `formula` | 公式计算(求和/均值/自定义) |
| 手写签名/签字 | `hand-sign` | 手写签名 |
| 大写金额/中文大写 | `capital-money` | 金额转大写 |
| 文本组合 | `text-compose` | 多字段值拼接显示 |
| 分隔符/分区 | `divider` | 表单区域分隔线 |
| 文本识别/OCR | `ocr` | 图片文字识别 |
| 子表/明细/清单 | `sub-table-design` | 设计子表 |
### Step 1.5: 字典数据源配置
对于 radio/select/checkbox 控件,数据源有两种方式:
**方式一:静态选项(默认)**
```json
"options": {
"remote": false,
"options": [
{ "value": "选项1", "itemColor": "#2196F3" },
{ "value": "选项2", "itemColor": "#08C9C9" }
]
}
```
**方式二:系统字典**
当用户描述中提到「字典」、「数据字典」或使用了 JeecgBoot 常见字典编码(如 sex、priority、valid_status 等),使用字典配置:
```json
"options": {
"remote": "dict",
"dictCode": "sex",
"showLabel": true,
"options": [],
"remoteOptions": [],
"props": { "value": "value", "label": "label" }
}
```
同时在**控件顶层**(与 options 同级)添加 `dictOptions`
```json
"dictOptions": [
{ "value": "1", "label": "男" },
{ "value": "2", "label": "女" }
]
```
**常用 JeecgBoot 系统字典编码:**
| 字典编码 | 说明 | 典型值 |
|---------|------|--------|
| `sex` | 性别 | 1=男, 2=女 |
| `priority` | 优先级 | L=低, M=中, H=高 |
| `valid_status` | 有效状态 | 0=无效, 1=有效 |
| `msg_category` | 消息类型 | 1=通知, 2=系统 |
| `send_status` | 发送状态 | 0=未发送, 1=已发送 |
| `yn` | 是否 | Y=是, N=否 |
> **提示:** 当用户指定的字典编码不确定是否存在时,可通过 API `GET /sys/dict/getDictItems/{dictCode}` 查询确认。如果用户只说了"用字典"但未指定编码,需要询问具体的字典编码。
**desform_utils.py 快捷函数使用字典的正确写法:**
> **踩坑警告:** `RADIO`/`SELECT`/`CHECKBOX` 的 `options` 是**必填位置参数**,使用字典时也不能省略。
> 当指定 `dict_code` 时,`options` 参数必须传**字典项列表**`[{value, label}]` 格式),不要传字符串列表。
> **不存在** `dict_options` 关键字参数,不要传它(会报 `unexpected keyword argument` 错误)。
```python
# 正确 ✅ — options 传字典项列表 + dict_code
RADIO('性别', [{'value': '1', 'label': ''}, {'value': '2', 'label': ''}], dict_code='sex')
SELECT('状态', [{'value': '0', 'label': '无效'}, {'value': '1', 'label': '有效'}], dict_code='valid_status')
# 错误 ❌ — 缺少 options 位置参数
RADIO('性别', dict_code='sex')
# 错误 ❌ — 不存在 dict_options 参数
RADIO('性别', ['', ''], dict_code='sex', dict_options=[...])
# 不用字典时options 传字符串列表即可
SELECT('职称', options=['教授', '副教授', '讲师', '助教'])
```
**底层 `make_widget` 函数中字典的实现原理(仅供参考):**
```python
# desform_utils.py 内部处理逻辑:
if dict_code:
opts["remote"] = "dict"
opts["dictCode"] = dict_code
opts["showLabel"] = True
opts["options"] = []
extra["dictOptions"] = options if isinstance(options[0], dict) else []
```
### Step 2: 展示表单摘要并确认
**必须展示以下内容,等待用户确认后再执行:**
```
## 表单摘要
- 表单名称:员工请假申请
- 表单编码yuan_gong_qing_jia_shen_qing
- 目标环境https://boot3.jeecg.com/jeecgboot
### 字段列表
| 序号 | 字段名称 | 控件类型 | 必填 | 说明 |
|------|---------|---------|------|------|
| 1 | 姓名 | input (单行文本) | 是 | 标题字段 |
| 2 | 请假类型 | select (下拉选择) | 是 | 选项:事假/病假/年假 |
| 3 | 开始日期 | date (日期) | 是 | |
| 4 | 结束日期 | date (日期) | 是 | |
| 5 | 请假天数 | integer (整数) | 是 | |
| 6 | 请假原因 | textarea (多行文本) | 否 | |
| 7 | 附件 | file-upload (文件上传) | 否 | |
确认以上信息正确?(y/n)
```
### Step 2.5: 检查表单编码是否已存在(防覆盖规则)
> **重要安全规则:** 在执行创建/保存操作之前,**必须**先通过 `get_form_id(code)` 检查表单编码是否已存在。
**检查方式:** 在临时脚本中调用 `get_form_id(code)` 或在创建脚本中加入检查逻辑。
**如果表单已存在:**
1. **不允许默认覆盖**(不要直接执行 `create_form``update_form`
2. 必须明确告知用户:`表单 {code} 已存在 (ID={id}),是否要覆盖更新?`
3. **只有用户明确确认后**才可以执行覆盖操作(调用 `update_form` 更新设计)
4. 如果用户拒绝覆盖,基于原编码生成 3~5 个新编码供用户选择(如原编码 `oa_leave_apply`,可提供 `oa_leave_apply_v2``oa_leave_request``oa_leave_form``oa_staff_leave` 等),用户选定后再重新生成
**预置脚本的防覆盖:** 所有 `scripts/` 目录下的脚本都内置了 `--force` 参数检查,不加 `--force` 时检测到已存在会自动退出。
**动态脚本的防覆盖:** 手动编写的临时脚本中,在调用 `create_form` 之前加入检查:
```python
existing_id, _ = get_form_id(code)
if existing_id:
print(f'表单 {code} 已存在 (ID={existing_id}),需要用户确认后才能覆盖')
sys.exit(1)
```
### Step 3: 生成 desformDesignJson 并调用 API
用户确认后,执行以下步骤:
#### 3.0 优先使用通用脚本 + JSON 配置(推荐方式)
> **重要:优先使用 `scripts/desform_creator.py` 通用脚本 + JSON 配置文件的方式,只需生成少量 JSON 数据即可创建表单,避免每次编写大量 Python 代码。只有当通用脚本无法满足特殊需求时,才编写自定义临时脚本。**
**脚本位置:** `scripts/desform_creator.py`
**使用步骤:**
1. 根据用户需求生成 JSON 配置文件Write 到工作目录的临时 `.json` 文件)
3. 用 Bash 执行脚本:`python "<skill目录>/scripts/desform_creator.py" --api-base <URL> --token <TOKEN> --config <config.json>`
4. 删除临时 JSON 配置文件
**脚本自动完成:**
- 防覆盖检查(不加 `--force` 时检测到已存在自动退出)
- 根据 JSON 配置构建所有控件
- 调用 `create_form` 创建/保存表单设计
- 输出菜单 SQL如果 JSON 中配置了 `menuParent`
**JSON 配置格式:**
```json
{
"formName": "表单中文名称",
"formCode": "module_form_code",
"layout": "word",
"titleIndex": 0,
"fields": [
{"name": "字段名", "type": "控件类型", ...}
],
"menuParent": "父菜单名称",
"menuIcon": "ant-design:appstore-outlined"
}
```
| JSON 字段 | 必填 | 默认值 | 说明 |
|-----------|------|--------|------|
| `formName` | 是 | - | 表单中文名称 |
| `formCode` | 是 | - | 表单编码(英文,模块名前缀) |
| `layout` | 否 | `"auto"` | 布局模式:`auto`/`half`/`full`/`word` |
| `titleIndex` | 否 | `0` | 标题字段在 fields 中的索引 |
| `fields` | 是 | - | 字段定义数组 |
| `menuParent` | 否 | - | 生成菜单 SQL 的父菜单名称 |
| `menuIcon` | 否 | `ant-design:appstore-outlined` | 父菜单图标 |
**字段定义fields 数组中每个对象):**
每个字段只需 `name` + `type`,其余参数可选:
```json
{"name": "工程名称", "type": "input", "required": true}
{"name": "工程类别", "type": "radio", "options": ["土建", "安装", "装饰"]}
{"name": "验收日期", "type": "date"}
{"name": "金额", "type": "money", "unit": "万元"}
{"name": "自动编号", "type": "auto-number", "prefix": "GCYS"}
{"name": "条码", "type": "barcode"}
{"name": "定位", "type": "location"}
{"name": "签字", "type": "hand-sign", "required": true}
{"name": "---", "type": "divider", "text": "分隔标题"}
{"name": "性别", "type": "radio", "dictCode": "sex",
"options": [{"value": "1", "label": "男"}, {"value": "2", "label": "女"}]}
```
**支持的 type 及可选参数:**
| type | 可选参数 | 说明 |
|------|---------|------|
| `input` | `required`, `placeholder`, `unique` | 单行文本 |
| `textarea` | `required` | 多行文本 |
| `number` | `required`, `unit`, `precision` | 数字 |
| `integer` | `required`, `unit` | 整数 |
| `money` | `required`, `unit` | 金额 |
| `date` | `required`, `fmt` | 日期fmt 默认 `yyyy-MM-dd` |
| `time` | `required` | 时间 |
| `switch` | - | 开关 |
| `slider` | - | 滑块 |
| `rate` | - | 评分 |
| `color` | - | 颜色 |
| `radio` | `options`(必填), `required`, `dictCode` | 单选 |
| `select` | `options`(必填), `required`, `multiple`, `dictCode` | 下拉 |
| `checkbox` | `options`(必填), `required`, `dictCode` | 多选 |
| `select-user` | `required`, `multiple` | 选人 |
| `select-depart` | `required`, `multiple` | 选部门 |
| `phone` | `required` | 手机 |
| `email` | `required` | 邮箱 |
| `area-linkage` | `required` | 省市级联 |
| `file-upload` | `required` | 文件上传 |
| `imgupload` | `required` | 图片上传 |
| `hand-sign` | `required` | 手写签名 |
| `auto-number` | `prefix` | 自动编号 |
| `barcode` | `codeType`(`barcode`/`qrcode`) | 条码 |
| `location` | `required` | 定位 |
| `formula` | `mode`, `expression`, `decimal`, `unit` | 公式 |
| `divider` | `text` | 分隔符name 会被忽略,用 text |
| `editor` | `required` | 富文本 |
| `markdown` | `required` | Markdown |
| `link-record` | `sourceCode`, `titleField`, `showFields`, `showMode`, `showType` | 关联记录 |
| `link-field` | `linkRecordKey`, `showField`, `fieldType`, `fieldOptions` | 他表字段 |
**完整示例(工程竣工验收申请表):**
```json
{
"formName": "工程竣工验收申请表",
"formCode": "eng_completion_acceptance",
"layout": "word",
"fields": [
{"name": "自动编号", "type": "auto-number", "prefix": "GCYS"},
{"name": "条码", "type": "barcode"},
{"name": "工程名称", "type": "input", "required": true},
{"name": "工程编号", "type": "input"},
{"name": "工程类别", "type": "radio", "options": ["土建工程", "安装工程", "装饰工程", "市政工程"]},
{"name": "建设单位", "type": "input"},
{"name": "工程地址", "type": "input"},
{"name": "施工单位", "type": "input"},
{"name": "开工时间", "type": "date"},
{"name": "完工时间", "type": "date"},
{"name": "工程量清单", "type": "textarea"},
{"name": "图片上传", "type": "imgupload"},
{"name": "定位", "type": "location"},
{"name": "验收类别", "type": "radio", "options": ["竣工验收", "分部验收", "专项验收"]},
{"name": "施工单位项目经理签字", "type": "hand-sign"},
{"name": "---", "type": "divider", "text": "广电工程完工验收报告"},
{"name": "工程名称(报告)", "type": "input"},
{"name": "工程编号(报告)", "type": "input"},
{"name": "建设单位(报告)", "type": "input"},
{"name": "施工单位(报告)", "type": "input"},
{"name": "开工时间(报告)", "type": "date"},
{"name": "完工时间(报告)", "type": "date"},
{"name": "验收时间", "type": "time"},
{"name": "验收类别(报告)", "type": "radio", "options": ["竣工验收", "分部验收", "专项验收"]},
{"name": "---", "type": "divider", "text": "竣工项目分项审查情况"},
{"name": "立项手续完整性", "type": "radio", "options": ["合格", "不合格", "整改后合格"]},
{"name": "项目主体组签字(立项)", "type": "hand-sign"},
{"name": "竣工资料完整性", "type": "radio", "options": ["合格", "不合格", "整改后合格"]},
{"name": "项目主体组签字(资料)", "type": "hand-sign"},
{"name": "施工工艺合规性", "type": "radio", "options": ["合格", "不合格", "整改后合格"]},
{"name": "项目主体组签字(工艺)", "type": "hand-sign"},
{"name": "技术指标达标情况", "type": "radio", "options": ["合格", "不合格", "整改后合格"]},
{"name": "项目主体组签字(技术)", "type": "hand-sign"},
{"name": "材料设备核定结果", "type": "radio", "options": ["合格", "不合格", "整改后合格"]},
{"name": "项目主体组签字(材料)", "type": "hand-sign"},
{"name": "工程量核量结果", "type": "radio", "options": ["合格", "不合格", "整改后合格"]},
{"name": "项目主体组签字(核量)", "type": "hand-sign"},
{"name": "验收问题清单", "type": "textarea"},
{"name": "验收结论", "type": "radio", "options": ["合格", "不合格", "整改后复验"]},
{"name": "技术部负责人签字", "type": "hand-sign"},
{"name": "施工单位签字", "type": "hand-sign"},
{"name": "分管领导组签字", "type": "hand-sign"}
],
"menuParent": "工程验收管理"
}
```
**调用示例:**
```bash
# 1. Write 工具生成 JSON 配置文件
# 2. 执行脚本
python "C:/Users/moe/.claude/skills/jeecg-desform/scripts/desform_creator.py" \
--api-base http://192.168.1.233:3100/jeecgboot \
--token eyJhbGciOiJIUzI1NiJ9... \
--config eng_acceptance.json
# 如需覆盖已存在的表单
python "C:/Users/moe/.claude/skills/jeecg-desform/scripts/desform_creator.py" \
--api-base http://192.168.1.233:3100/jeecgboot \
--token eyJhbGciOiJIUzI1NiJ9... \
--config eng_acceptance.json \
--force
# 3. 删除临时 JSON 文件
```
#### 3.1 生成唯一标识
- key 和 model 使用当前时间戳毫秒数 + 6 位随机数
- 格式参见 `references/desform-design-json-schema.md`
#### 3.2 构造 desformDesignJson
阅读以下参考文件(按需):
- `references/desform-design-json-schema.md` — JSON Schema 结构、控件类型清单、通用字段(必读)
- `references/desform-widget-options.md` — 每种控件的完整 options 配置(必读)
- `references/desform-examples.md` — 常见表单模式示例 + Python 脚本模板(必读)
- `references/desform-real-samples.md` — 真实业务表单案例(字典、半行、分区、公式、关联)
核心要点:
- 每个普通控件必须包裹在 `card` 容器中(除了 editor、markdown、divider、map、sub-table-design、link-record(多条/表格模式)、grid、tabs
- `config.titleField` 指向标题字段的 model优先 input也可以是 select-user 等其他控件)
- `config.hasWidgets` 必须列出所有使用到的控件 type包括 card
- key 格式:`{timestamp}_{6位随机数}`(源码中实际是 randomKey但时间戳格式也兼容
- model 格式:`{type}_{timestamp}_{6位随机数}`type 中的 `-` 转为 `_`,如 `link_record_xxx`
- model 必须全局唯一(保存时会检查重复 model
**className / icon 易错控件(实测验证):**
- `link-record`: className=`form-link-record`, icon=**`icon-link`**(不是 `icon-link-record`
- `link-field`: className=`form-link-field`, icon=**`icon-field`**(不是 `icon-link-field`
- `sub-table-design`: className=**`form-sub-table`**, icon=**`icon-table`**(不是 `form-sub-table-design` / `icon-sub-table-design`
**link-record / link-field 关键配置:**
- link-record 的 `advancedSetting.defaultValue.customConfig` 必须为 `true`
- link-record 的 `allowView``allowEdit``allowAdd``allowSelect` 必须全部设为 `true`4 个操作选项默认全部勾选)
- link-record 的 `titleField` 必须填源表真实标题字段 model`showFields` 填源表展示字段 model 列表
- link-field **没有 `advancedSetting`**(与其他控件不同)
- link-field 的 `linkRecordKey` 填 link-record 的 **key**(不是 model
- link-field 的 `fieldType` 必须填源字段的真实控件类型(不能一律写 `"input"`
- link-field 的 `fieldOptions` 需包含源字段类型相关的 options如 select-user 需 `{"multiple": false, "customReturnField": "username"}`
**sub-table-design 关键配置:**
- options 必须包含 `allowAdd: true`,否则子表没有"添加"按钮
- 完整 options 见 `references/desform-widget-options.md`showCheckbox、showNumber、operationMode 等缺一不可)
- 子表内可放 link-record + link-field 实现行级关联选择
**跨表单批量创建流程:**
1. 先创建基础表单 → 2. 查询获取字段 model → 3. 构建业务表单时引用这些 model
#### 3.3 使用 Python 调用 API必须用 Python不要用 curl
**优先使用共通工具库 `desform_utils.py`**(位于 `scripts/desform_utils.py`)。
**使用共通工具库的执行步骤:**
```
1. Write 工具 → 写入业务脚本 create_xxx.pyscripts/ 目录import desform_utils
2. Bash 工具 → cd <skill目录>/scripts && python create_xxx.py
3. Bash 工具 → rm create_xxx.py清理临时脚本
```
**共通工具库使用示例:**
```python
import sys
sys.path.insert(0, r'{后端项目根目录}')
from desform_utils import *
init_api('https://boot3.jeecg.com/jeecgboot', 'your-token')
# 简单表单(含字典用法)
create_form('员工信息', 'employee_info', [
INPUT('姓名', required=True),
RADIO('性别', [{'value': '1', 'label': ''}, {'value': '2', 'label': ''}], dict_code='sex'),
PHONE('电话'),
EMAIL('邮箱'),
DEPART('部门'),
SELECT('职称', options=['教授', '副教授', '讲师', '助教']),
TEXTAREA('备注'),
])
# 带关联的表单
form_id, title = create_form('客户信息', 'customer_info', [
INPUT('客户名称', required=True),
PHONE('电话'),
])
# 查询字段用于关联
tf, fields = get_form_fields('customer_info')
create_form('联系人', 'contact_info', [
INPUT('姓名', required=True),
LINK_RECORD('所属客户', 'customer_info', tf, [fields['客户名称']['model']]),
])
# 菜单SQLID 自动生成 UUID只需传菜单名和子项
print(gen_menu_sql('CRM系统', [
('客户信息', 'customer_info', 1),
('联系人', 'contact_info', 2),
]))
# 查询表单
form = query_form('customer_info')
print(form['id'], form['updateCount'])
# 修改已有表单设计(自动获取 updateCount
update_form('customer_info', [
INPUT('客户名称', required=True),
PHONE('电话'),
EMAIL('邮箱'),
TEXTAREA('备注'),
])
# 删除表单(支持 3 种方式)
delete_form('customer_info') # 传 code自动查找 ID
delete_form('customer_info', '123456789') # 传 code + 已知 ID跳过搜索最快
delete_form('123456789012345678') # 只传 ID
```
**可用的快捷函数(大写命名):**
- 基础: `INPUT`, `TEXTAREA`, `NUMBER`, `INTEGER`, `MONEY`, `DATE`, `TIME`, `SWITCH`, `SLIDER`, `RATE`, `COLOR`
- 选择: `RADIO`, `SELECT`, `CHECKBOX`(支持 dict_code 字典)
- 系统: `USER`, `DEPART`, `PHONE`, `EMAIL`, `AREA`
- 文件: `FILE`, `IMGUPLOAD`, `HANDSIGN`
- 高级: `AUTONUMBER`, `FORMULA`, `LINK_RECORD`, `LINK_FIELD`
- 不需要 card: `DIVIDER`, `EDITOR`, `MARKDOWN`
- 子表内: `SUB_INPUT`, `SUB_INTEGER`, `SUB_NUMBER`, `SUB_MONEY`, `SUB_SELECT`, `SUB_DATE`, `SUB_LINK_RECORD`, `SUB_LINK_FIELD`, `SUB_FORMULA`
- 容器: `make_card`, `make_sub_table`
- API: `init_api`, `create_form`, `update_form`, `delete_form`, `query_form`, `get_form_id`, `get_form_fields`, `find_or_create_form`, `save_design`
> **`create_form` 的 `layout` 参数:**
> - `'auto'`(默认):字段数 >= 6 时自动使用半行两列布局
> - `'half'`:强制半行布局
> - `'full'`:强制整行布局(不做半行处理)
> - `'word'`Word 风格布局(表格边框样式,见下方详细说明)
> - textarea/editor/file-upload/imgupload 等宽控件自动保持整行
>
> **Word 风格表单(`layout='word'`**
>
> Word 风格模拟传统 Word 文档表格样式,适用于审批单、申请表等正式场景。
>
> **实现原理JeecgBoot 表单设计器内置支持):**
> - `formStyle: "word"` — 表单风格设为 Word设计器右侧「表单属性」→「表单风格」→「Word风格」
> - 栅格布局 `grid`className = `form-grid form-grid-word-theme` — 每行一个栅格容器
> - 标签列:独立的 `text` 控件16px、居中放在栅格的第一列
> - 控件列:实际控件设置 `hideTitle: true`(隐藏标题),放在栅格的第二列
> - 顶部标题:独立的 `text` 控件24px、加粗、居中不使用内置 header
> - 外部 CSS加载 `/desform/expand/css/theme-word.css` 提供表格边框样式
> - `showHeaderTitle: false`、`disabledAutoGrid: true`
>
> **栅格 span 分配规则:**
> - 两列行半行控件配对标签1 span=6 + 控件1 span=6 + 标签2 span=4 + 控件2 span=8
> - 单列行textarea/file-upload 等宽控件):标签 span=6 + 控件 span=18
>
> **使用示例:**
> ```python
> create_form('提成申请单', 'oa_commission_apply', [
> USER('申请人', required=True),
> DEPART('部门', required=True),
> DATE('申请日期', required=True),
> INPUT('项目名称', required=True),
> MONEY('合同金额', required=True),
> MONEY('提成金额', required=True),
> TEXTAREA('提成说明'),
> FILE('附件'),
> ], layout='word')
> ```
>
> **注意事项:**
> - `_apply_word_layout` 会自动生成顶部标题 text、栅格行、text 标签
> - hand-sign/textarea/file-upload/divider 等宽控件自动独占一行
> - 标签列 flex 垂直居中对齐
>
> **`gen_menu_sql` 的 `icon` 参数:**
> - 默认值 `'ant-design:appstore-outlined'`,一级菜单自动带图标
> - 可自定义:`gen_menu_sql('费用管理', [...], icon='ant-design:dollar-outlined')`
- 字典: `query_dict(code)` 查询字典项, `search_dict(keyword)` 按名称/编码模糊搜索字典
- SQL: `gen_menu_sql`
**如果共通工具库不存在,则使用以下方式:**
**重要限制(实战踩坑):**
1. **Windows 环境下 curl 发送中文/长JSON会出错**,必须使用 Python 的 urllib/requests 确保 UTF-8 编码
2. **禁止使用 `python3 -c "..."` 内联方式**,因为 JSON 中的特殊字符会被 bash 解析出错
3. **必须先用 Write 工具写入 `.py` 临时文件,再用 Bash 执行,最后删除临时文件**
**执行步骤:**
```
1. Write 工具 → 写入 create_desform.py项目根目录
2. Bash 工具 → python create_desform.py
3. Bash 工具 → rm create_desform.py清理
```
**API 踩坑记录(实战验证):**
> **关键踩坑:**
> 1. `POST /desform/add` 现已直接返回表单实体(含 ID`desform_utils.py` 已优先从返回值获取 ID旧版后端不返回时自动 fallback 到 list 搜索
> 2. `GET /desform/queryByCode` **不可靠**(部分表单查不到),推荐用 `GET /desform/queryByIdOrCode?desformCode={code}`
> 3. `queryByIdOrCode` 对新创建但未保存设计的表单也可能返回失败,此时需通过 list API 全量搜索
> 4. list API 的 `desformCode` 过滤参数**不可靠**(有时匹配不到),必须全量搜索后手动精确匹配
> 5. `PUT /desform/edit` 的 `updateCount` 必须传**当前数据库中的值**(不是 +1后端会自动递增
> 6. `DELETE /desform/deleteBatch` 是**逻辑删除**(放入回收站),表单 code 仍被占用
> 7. `DELETE /desform/recycleBin/deleteByIds` 可彻底删除回收站中的表单,释放 code。`delete_form` 已封装完整流程,支持传 code 或 ID
> 8. `PUT /desform/recycleBin/recoverByIds` 可从回收站恢复表单
> 9. `DELETE /desform/recycleBin/empty` 清空回收站(在演示环境中可能不完全生效)
> 10. **删除后重建时序问题:** 彻底删除表单后code 释放可能有延迟。如果 `add` 返回 `该code已存在`,说明该 code 之前被另一个表单占用(同 code 可能存在多条记录)。此时应通过 list 全量搜索找到占用该 code 的表单,对其执行 `deleteBatch` + `recycleBin/deleteByIds` 彻底删除后再重建
> 11. **`save_design` 报「未找到对应实体」:** 通常是因为使用了已被删除的旧表单 ID。`find_or_create_form` 可能返回旧 ID缓存或竞态此时需通过 list API 重新搜索获取最新有效 ID
>
> **`create_form` vs `save_design` 使用区别:**
> - **推荐始终使用 `create_form`**(一站式:查找/创建 + 保存设计),它会自动解包 tuple、确定标题字段、处理 updateCount
> - `save_design` 是底层函数,签名为 `save_design(form_id, form_code, widgets, title_model, update_count)`
> - `widgets` 参数需要传**解包后的 widget dict 列表**(不是 tupletuple 需先 `[w[0] for w in widgets_tuples]` 解包
> - `title_model` 是标题字段的 model 字符串(不是 index可通过 `widgets_tuples[0][2]` 获取
> - 如需直接调用 `save_design`,务必先通过 `queryByIdOrCode` 获取最新 `updateCount`
>
> **命名规则:**
> - 表单编码使用英文命名(不用拼音),模块名作为前缀
> - 格式:`{模块}__{实体}`,如 `crm_customer`、`crm_contact`、`oa_leave_apply`
> - 同一模块的表单共享前缀,便于分组管理
>
> **find_or_create_form 策略desform_utils.py 中已实现):**
> 1. 先尝试 `POST /desform/add` 创建
> 2. 若 add 成功且返回值含 ID → 直接使用(新版后端已支持)
> 3. 若 add 成功但返回值无 ID → 通过 list API 全量搜索获取 ID旧版兜底
> 4. 若 add 失败code已存在→ 尝试 `queryByIdOrCode` 获取 ID
> 5. 若 queryByIdOrCode 也失败 → 通过 list API 全量搜索获取 ID
#### 3.4 检查结果
- `success: true` → 表单创建成功
- `success: false` → 输出错误信息,检查 desformCode 是否重复等
### Step 4: 输出结果
```
## 表单创建成功
- 表单ID{id}
- 表单名称:{desformName}
- 表单编码:{desformCode}
- 目标环境:{API_BASE}
请在表单设计器中查看:打开 JeecgBoot 后台 → 表单设计器 → 找到该表单
```
**同时输出菜单 + 角色授权 SQL用于将设计器表单加入系统菜单**
`gen_menu_sql` 函数会同时生成 `sys_permission`(菜单)和 `sys_role_permission`(角色授权)的 SQL。
**所有 ID菜单 ID、授权记录 ID均自动生成 32 位无横线 UUID无需手动指定。**
```python
# 调用方式:只需传父菜单名称 + 子菜单列表
sql = gen_menu_sql('物业管理', [
('小区信息', 'pm_community', 1),
('楼栋信息', 'pm_building', 2),
('房屋信息', 'pm_house', 3),
])
print(sql)
```
生成的 SQL 格式(每条 INSERT 都带完整列名,避免列错位):
```sql
-- 父菜单ID 自动生成 UUID
INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
VALUES ('{uuid}', NULL, '{parentName}', '/{uuid}', 'layouts/RouteView', NULL, NULL, 0, NULL, '1', 1.00, 0, NULL, 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', now(), NULL, NULL, 0);
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip)
VALUES ('{uuid}', '{roleId}', '{parentUuid}', NULL, now(), '127.0.0.1');
-- 子菜单ID 自动生成 UUID
INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
VALUES ('{uuid}', '{parentUuid}', '{desformName}', '/online/desform/list/{desformCode}', 'super/online/desform/auto/AutoDesformDataList', 'AutoDesformDataList', NULL, 0, NULL, '1', 1.00, 0, NULL, 0, 1, 0, 0, 0, NULL, '1', 0, 0, 'admin', now(), NULL, NULL, 0);
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip)
VALUES ('{uuid}', '{roleId}', '{menuUuid}', NULL, now(), '127.0.0.1');
```
**菜单 SQL 关键字段说明:**
| 字段 | 值 | 说明 |
|------|-----|------|
| id | 自动生成 32 位 UUID | 如 `d0ca42ae976a4dfbbff491e304858fe1` |
| url | `/online/desform/list/{desformCode}` | 设计器表单数据列表路由desformCode 是表单编码 |
| component | `super/online/desform/auto/AutoDesformDataList` | 固定值,设计器表单自动数据列表组件 |
| component_name | `AutoDesformDataList` | 固定值 |
| is_route | `0` | 不走普通路由 |
| is_leaf | `1` | 叶子节点 |
| parent_id | `NULL` 或父菜单UUID | NULL=一级菜单指定父UUID=子菜单 |
**角色授权 SQL 说明:**
| 字段 | 值 | 说明 |
|------|-----|------|
| id | 自动生成 32 位 UUID | 每条授权记录独立 UUID |
| role_id | `f6817f48af4fb3af11b9e8bf182f618b` | 默认角色 IDdesform_utils.py 中 ROLE_ID 常量),可通过参数覆盖 |
| permission_id | 对应的菜单 UUID | 关联 sys_permission.id |
> **重要:输出菜单 SQL 时,必须直接使用 `gen_menu_sql` 函数的完整输出,不要手动缩写或省略列名,否则会因列错位导致执行报错。**
### 本地环境自动执行菜单 SQL 规则
**判断条件:** `init_api` 传入的 api_base 以 `http://127.0.0.1``http://localhost` 开头(不区分大小写)。
**自动执行方式:**`gen_menu_sql` 生成 SQL 后,通过 Bash 工具逐条执行 MySQL 命令:
```bash
# 先检查菜单是否已存在,避免重复插入
mysql -h127.0.0.1 -P3306 -uroot -proot jeecgboot3 -e "SELECT id FROM sys_permission WHERE id='{menuId}'"
# 不存在则执行插入(包括 sys_permission 和 sys_role_permission
mysql -h127.0.0.1 -P3306 -uroot -proot jeecgboot3 -e "INSERT INTO sys_permission(...) VALUES (...);"
mysql -h127.0.0.1 -P3306 -uroot -proot jeecgboot3 -e "INSERT INTO sys_role_permission(...) VALUES (...);"
```
**注意事项:**
-`gen_menu_sql` 的每条 INSERT 语句拆分后逐条通过 MySQL CLI 执行
- 执行前先检查父菜单 ID 是否已存在,避免重复插入
- 如果 MySQL 执行失败,回退为输出 SQL 让用户手动执行,不中断整体流程
- 数据库连接参数默认 `mysql -h127.0.0.1 -P3306 -uroot -proot jeecgboot3`,与 jeecg-codegen 保持一致
- 输出结果中标注 `菜单 SQL已自动执行 ✓`
---
## 编辑已有表单
如果用户要修改已有表单,需提供表单 ID 或编码,然后:
1. 查询现有表单设计 JSON
2. 根据用户需求修改 JSON
3. 调用 `PUT /desform/edit` 保存(注意带上正确的 `updateCount`
---
## 删除表单
`delete_form` 已封装完整的删除流程(查找 → 逻辑删除 → 物理删除),支持 3 种调用方式:
```python
from desform_utils import *
init_api('https://boot3.jeecg.com/jeecgboot', 'your-token')
# 方式1传 code自动查找 ID优先 queryByIdOrCode 快速查找)
delete_form('edu_teacher')
# 方式2传 code + 已知 ID跳过搜索最快
delete_form('edu_teacher', '2032994312457920514')
# 方式3只传 ID纯数字且长度>15 自动识别为 ID
delete_form('2032994312457920514')
```
**内部执行流程:**
1. 确定表单 ID传了 ID 直接用,传 code 则优先 `queryByIdOrCode` 快速查找,查不到再 fallback 到 list 全量搜索)
2. `DELETE /desform/deleteBatch?ids={id}` — 逻辑删除(放入回收站)
3. `DELETE /desform/recycleBin/deleteByIds?ids={id}` — 物理删除
**删除注意事项:**
- **不能跳过逻辑删除:** `recycleBin/deleteByIds` 只删除 `del_flag=1` 的记录,必须先执行 `deleteBatch`
- **同一 code 可能存在多条记录:** 传 code 时会自动处理多条记录全部删除
- **批量删除时传 ID 更快:** 创建时已获取 ID删除时直接传入可跳过查询
---
## 错误处理
| 错误 | 解决方案 |
|------|---------|
| Token 过期401/认证失败) | 提示用户重新获取 X-Access-Token |
| `该code已存在` | **不要直接覆盖**,提示用户确认是否覆盖(参见 Step 2.5 防覆盖规则),用户确认后再用 `update_form` 更新设计 |
| `未找到对应实体` | 表单数据不一致(存在于 list 但无法编辑),需用 `deleteBatch` + `recycleBin/deleteByIds` 彻底删除后重建 |
| `表单编码过长` | desformCode 缩短到 200 字符以内 |
| `当前版本已过时,请刷新重试` | updateCount 传值错误,必须传当前值(通过 queryByIdOrCode 或 list 获取) |
| `add` 返回 `result: null` | 旧版后端行为,`desform_utils.py` 已自动 fallback 到 list 搜索;新版后端已直接返回实体 |
| `queryByCode` 返回 false | 该接口不可靠,改用 `queryByIdOrCode` 或 list 全量搜索 |
| 中文乱码 | 确认使用 Python urllib不要用 curl |
| 连接超时 | 确认后端地址可达,检查网络 |
## 参考文档
- `scripts/desform_creator.py`**通用表单创建脚本**,优先使用此脚本 + JSON 配置文件
- `scripts/desform_utils.py`**共通工具库**控件工厂、API 封装、布局引擎)
- `references/desform-design-json-schema.md` — JSON Schema 结构、控件类型清单、通用字段
- `references/desform-widget-options.md` — 每种控件的完整 options 配置
- `references/desform-examples.md` — 常见表单模式示例 + Python 脚本模板
- `references/desform-real-samples.md` — 真实业务表单案例(字典、半行、分区、公式、关联)

View File

@@ -0,0 +1,137 @@
# jeecg-aiform 使用指南
## 快速开始
### 1. 自然语言描述表单
直接告诉 AI 你需要什么表单:
```
帮我创建一个请假申请表单,包含:
- 姓名(必填)
- 请假类型(事假/病假/年假,单选)
- 开始日期
- 结束日期
- 请假天数
- 请假原因
- 附件
```
### 2. 提供连接信息
AI 会要求你提供:
- **后端地址**:如 `https://boot3.jeecg.com/jeecgboot`
- **X-Access-Token**:从浏览器 F12 → Network → 任意请求的 Headers 中复制
### 3. 确认后自动创建
AI 会展示表单摘要(字段列表、控件类型),确认后自动调用 API 创建。
---
## 多种描述方式
以下描述方式都会触发 AI 表单生成:
| 描述方式 | 示例 |
|---------|------|
| 直接说创建表单 | "创建一个员工信息表单" |
| 描述字段需求 | "我需要一个表单,有姓名、手机号、地址" |
| 描述业务场景 | "做一个采购单,包含采购明细子表" |
| 简短指令 | "AI设计表单报销申请" |
| 英文指令 | "generate a leave application form" |
---
## 支持的控件类型
### 基础字段
- 单行文本、多行文本、数字、整数、金额
### 选择字段
- 单选框、多选框、下拉选择、下拉树
### 日期时间
- 日期选择器、时间选择器、日期范围
### 交互控件
- 开关、评分、滑块、颜色选择器
### 上传控件
- 图片上传、文件上传
### 联系方式
- 手机、邮箱
### 富文本
- 富文本编辑器、Markdown 编辑器
### 高级字段
- 省市级联动、地图、定位、条码、自动编号、文本组合
### 人员组织
- 用户选择、部门选择、岗位选择、组织角色
### 关联控件
- 关联记录、他表字段、汇总
### 布局控件
- 设计子表(明细表)
---
## 修改已有表单
如果要修改已创建的表单:
```
修改表单 xxx增加一个"审批意见"多行文本字段
```
需要提供表单 ID 或编码。
---
## 常见场景示例
### 员工信息登记
```
创建员工信息登记表单:姓名(必填)、工号(必填)、手机、邮箱、部门、入职日期、备注、头像照片
```
### 请假申请
```
做一个请假申请表单:
- 申请人(用户选择,默认当前登录人)
- 请假类型(单选:事假/病假/年假/调休)
- 开始日期、结束日期
- 请假天数(整数)
- 请假原因(多行文本,必填)
- 附件
```
### 采购申请(带子表)
```
创建采购申请单:
主表字段:采购标题(必填)、采购部门、采购日期、总金额、备注
子表明细:物品名称、规格型号、数量(整数)、单价(金额)、小计(金额)
```
### 客户信息管理
```
创建客户信息表单:
- 客户名称(必填)、联系人、手机、邮箱 — 前两个一行两字段
- 所在地区(省市级联动)
- 客户类型(下拉:潜在客户/意向客户/成交客户)
- 客户等级评分5星
- 备注(富文本编辑器)
```
---
## 注意事项
1. **Token 有效期**X-Access-Token 有过期时间,过期后需重新获取
2. **表单编码唯一**:同一系统中 desformCode 不能重复
3. **设计器表单 vs Online 表单**:本 skill 只处理设计器表单desform不涉及 Online 表单
4. **创建后可在设计器中微调**AI 创建的表单可以在设计器界面中继续编辑完善

View File

@@ -0,0 +1,322 @@
# desformDesignJson Schema 参考
## 顶层结构
```json
{
"list": [ /* */ ],
"config": { /* */ }
}
```
## 全局配置config完整字段
```json
{
"formStyle": "normal",
"titleField": "input_xxx",
"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": ["input", "card", "textarea"],
"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
}
```
**关键字段说明:**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `titleField` | String | 是 | 标题字段的 model key列表页显示用通常指向第一个 input |
| `hasWidgets` | String[] | 是 | 已使用的所有控件类型(包括 `card`),自动维护 |
| `labelPosition` | String | 否 | 标签位置:`"top"` / `"left"` / `"right"` |
| `size` | String | 否 | 控件尺寸:`"small"` / `"default"` / `"large"` |
## 控件列表list
list 是控件数组。**大部分控件被包裹在 `card` 容器中**。
### card 容器结构
```json
{
"key": "{timestamp}_{random6}",
"type": "card",
"isAutoGrid": true,
"isContainer": true,
"list": [ /* 1~2 */ ],
"options": {},
"model": "card_{timestamp}_{random6}"
}
```
### 控件通用结构
```json
{
"type": "input",
"name": "字段标签",
"className": "form-input",
"icon": "icon-input",
"hideTitle": false,
"options": { /* */ },
"advancedSetting": {
"defaultValue": {
"type": "compose",
"value": "",
"format": "string",
"allowFunc": true,
"valueSplit": "",
"customConfig": false
}
},
"remoteAPI": { "url": "", "executed": false },
"key": "{timestamp}_{random6}",
"model": "{type}_{timestamp}_{random6}",
"modelType": "main",
"rules": [],
"isSubItem": false
}
```
**通用字段:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | String | 控件类型标识 |
| `name` | String | 控件显示名称(标签) |
| `className` | String | CSS 类名 |
| `icon` | String | 图标类名 |
| `hideTitle` | Boolean | 是否隐藏标题 |
| `hideLabel` | Boolean | 是否隐藏标签divider、text、buttons 等为 `true` |
| `options` | Object | 控件特有配置 |
| `advancedSetting` | Object | 高级默认值设置 |
| `remoteAPI` | Object | 远程数据源 |
| `key` | String | 唯一标识 |
| `model` | String | 数据绑定 Key |
| `modelType` | String | `"main"``"sub_one2one"` |
| `rules` | Array | 校验规则(必填时加 `[{"required": true, "message": "${title}必须填写"}]` |
| `defaultRules` | Array | 控件自带的默认校验phone/email/rate 等自动生成,无需手动设置) |
| `isSubItem` | Boolean | 是否为子表内控件 |
| `subOptions` | Object | 子表内控件专用:`{"width": "200px", "parentKey": "子表key"}` |
| `jeecg_auth` | Object | 权限控制:`{"enabled": true, "title": "名称", "field": "model值"}` |
| `mobileOptions` | Object | 移动端覆盖配置(可选,同 options 结构,仅移动端生效) |
| `dictOptions` | Array | 字典选项(使用字典数据源时,与 options 同级) |
| `event` | Object | 事件处理buttons 等控件):`{"click": "console.log('hello')"}` |
## key 和 model 生成规则
源码中 key 由 `randomKey()` 生成12-18 位随机字符串model 由 `type + '_' + key` 派生(中划线转下划线)。但 **时间戳+随机数** 格式同样被接受,两种方式都有效:
| 方式 | key 示例 | model 示例 |
|------|---------|------------|
| 源码 randomKey | `nRk92kK92sk` | `input_nRk92kK92sk` |
| 时间戳+随机数 | `1773452631695_489584` | `input_1773452631695_489584` |
**model 生成规则(源码 widgetUtils.js**
```javascript
let model = widget.type + '_' + key
model = model.replace(/-/g, '_') // 中划线 → 下划线
// link-record → link_record_xxx
```
**Python 生成方法(使用时间戳格式,实测可用):**
```python
import time
import random
def gen_key():
ts = int(time.time() * 1000)
rnd = random.randint(100000, 999999)
return f"{ts}_{rnd}"
def gen_model(widget_type):
ts = int(time.time() * 1000)
rnd = random.randint(100000, 999999)
safe_type = widget_type.replace('-', '_')
return f"{safe_type}_{ts}_{rnd}"
```
> 注意:每个控件的 key 和 model 必须全局唯一(保存时会检查重复 model。card 容器的 key/model 与内部控件的 key/model 也必须不同。
## AutoGrid 机制(自动栅格)
设计器中启用自适应(`config.disabledAutoGrid: false`)时,拖入非容器控件会自动包裹一个 `card``isAutoGrid: true`)。
### 不进入 AutoGrid 的控件(不需要 card 包裹)
| 控件 type | 说明 |
|-----------|------|
| `editor` | 富文本编辑器 |
| `markdown` | Markdown 编辑器 |
| `divider` | 分隔符 |
| `map` | 地图 |
| `link-record``showType: "table"``isSubTable: true` | 关联记录表格/子表模式 |
| `sub-table-design` | 设计子表 |
| `grid` | 栅格布局(本身是容器) |
| `card` | 卡片(本身是容器) |
| `tabs` | 选项卡(本身是容器) |
### 需要 card 容器的控件
所有其他控件,包括:
`input`, `textarea`, `number`, `integer`, `money`, `radio`, `checkbox`, `select`, `select-tree`, `date`, `time`, `switch`, `rate`, `color`, `slider`, `phone`, `email`, `imgupload`, `file-upload`, `buttons`, `text`, `area-linkage`, `location`, `capital-money`, `barcode`, `text-compose`, `auto-number`, `formula`, `hand-sign`, `ocr`, `link-record`showMode="single" 且 showType 非 table, `link-field`, `summary`, `select-user`, `select-depart`, `select-depart-post`, `org-role`
## 半行布局
一个 card 内放 2 个控件可实现半行布局,每个控件的 `options.autoWidth` 设为 `50`
```json
{
"type": "card",
"isAutoGrid": true,
"isContainer": true,
"list": [
{ "type": "input", "name": "姓名", "options": { "autoWidth": 50, ... }, ... },
{ "type": "phone", "name": "手机", "options": { "autoWidth": 50, ... }, ... }
],
...
}
```
## advancedSetting 的 format 值
| 控件数据类型 | format 值 |
|-------------|-----------|
| 文本类input、textarea、select、radio 等) | `"string"` |
| 数字类number、integer、money、slider | `"number"` |
| 多选带分隔checkbox、多选 select | `"string"` + `valueSplit: ","` + `customConfig: true` |
## rules 校验规则
**必填字段:**
```json
"rules": [{ "required": true, "message": "${title}必须填写" }]
```
**同时需要将 options 中的 required 设为 true。**
**自带校验的控件(有 defaultRules 字段):**
- `phone` — 自带手机号校验
- `email` — 自带邮箱校验
- `rate` — 自带 validator
## advancedSetting 详解
```json
{
"defaultValue": {
"type": "compose", // compose(静态+组合) | function(函数) | javascript(自定义JS) | linkage(关联查询) | none(无)
"value": "", // 默认值内容
"format": "string", // string | number | boolean
"allowFunc": true, // 是否允许在默认值中使用函数
"valueSplit": ",", // 多选控件的值分割符checkbox, radio, select 多选)
"customConfig": false // 是否需要自定义配置界面
}
}
```
**customConfig 为 true 的控件:** select、radio、checkbox多选场景、link-record、sub-table-design
## 布局容器结构
### grid — 栅格布局
```json
{
"type": "grid",
"isContainer": true,
"columns": [
{
"span": 12,
"options": {
"flex": false,
"flexAlignItems": "flex-start",
"flexJustifyContent": "start"
},
"list": []
},
{ "span": 12, "list": [] }
],
"options": {
"gutter": 8,
"justify": "start",
"align": "top",
"isWordStyle": false,
"hidden": false
}
}
```
### tabs — 选项卡
```json
{
"type": "tabs",
"isContainer": true,
"panes": [
{
"name": "Tab_xxx",
"label": "Tab1",
"rowNum": 1,
"hidden": false,
"hiddenOnAdd": false,
"list": []
}
],
"options": {
"width": "100%",
"activeName": "Tab_xxx",
"type": "border-card",
"position": "top",
"hidden": false
}
}
```
## 完整控件类型清单
### 基础组件
`input`, `textarea`, `number`, `integer`, `money`, `radio`, `checkbox`, `time`, `date`, `rate`, `color`, `select`, `switch`, `slider`
### 高级组件
`phone`, `email`, `imgupload`, `file-upload`, `editor`, `markdown`, `buttons`, `text`, `divider`, `area-linkage`, `map`, `location`, `capital-money`, `barcode`, `text-compose`, `auto-number`, `formula`, `hand-sign`, `ocr`
### 关联组件
`link-record`, `link-field`, `sub-table-design`, `table-dict`, `select-tree`, `summary`
### 布局组件
`grid`, `card`, `tabs`
### 系统组件
`select-user`, `select-depart`, `select-depart-post`, `org-role`
### OA 组件
`oa-approval-comments`, `x_oa_timeout_date`, `x_oa_official_doc_no`, `oa-sign-holiday-select`, `oa-leave-date-select`, `oa-sign-patch-select`

View File

@@ -0,0 +1,798 @@
# 常见表单模式示例
## 模式 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 和 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 基础上,字段构建部分替换为:
```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/editupdateCount 传当前值
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 中默认全部开启

View File

@@ -0,0 +1,288 @@
# 真实表单设计案例参考
> 从 JeecgBoot 演示系统 `design_form` 表中提取的真实表单配置,涵盖 OA、HR、行政等常见业务场景。
---
## 1. 字典翻译示例 (demo_test_dict_transl)
**场景:** 展示 radio/checkbox/select 的三种数据源(静态、字典、远程)
**控件清单18个**
- 单行文本 (input)
- 单选框组_远程数据 (radio) — `remote: true, remoteFunc: "http://..."`
- 单选框组_静态数据 (radio) — `showLabel: true, options: [{value:"1",label:"数学"}, ...]`
- 单选框组 (radio) — **`remote: "dict", dictCode: "sex"`**
- 多选框组 (checkbox) — **`remote: "dict", dictCode: "sex"`**
- 多选框组_静态数据 (checkbox) — 静态选项
- 下拉选择框 (select) — `showLabel: true, options: [{value:"1",label:"选项1"}, ...]`
- 下拉选择框_多选 (select) — `multiple: true`
- 下拉选择框_字典 (select) — **`remote: "dict", dictCode: "urgent_level"`**
- 开关 (switch)
- 省市级联动 (area-linkage)
- 用户组件 (select-user) — `multiple: true`
- 部门组件 (select-depart) — `multiple: true`
- 下拉树_分类字典 (select-tree) — `multiple: true`
- 下拉树_表 (select-tree) — `multiple: true`
- 表字典_popup (table-dict) — `multiple: true`
- 表字典_模糊online (table-dict) — `multiple: true`
- 表字典_模糊表 (table-dict) — `multiple: true`
**关键配置模式 — 字典 radio**
```json
{
"type": "radio",
"name": "单选框组",
"options": {
"inline": true,
"showLabel": true,
"remote": "dict",
"dictCode": "sex",
"options": [
{"value": "选项1", "itemColor": "#e9e9e9"},
{"value": "选项2", "itemColor": "#e9e9e9"}
],
"remoteOptions": [],
"props": {"value": "value", "label": "label"},
"remoteFunc": "",
"useColor": false,
"showType": "default",
"colorIteratorIndex": 0,
"matrixWidth": 120
},
"advancedSetting": {
"defaultValue": {
"type": "compose", "value": "", "format": "string",
"allowFunc": true, "valueSplit": ",", "customConfig": true
}
}
}
```
**关键配置模式 — 静态 radio (showLabel+value/label)**
```json
{
"type": "radio",
"options": {
"showLabel": true,
"remote": false,
"options": [
{"value": "1", "label": "数学", "itemColor": "#e9e9e9"},
{"value": "2", "label": "语文", "itemColor": "#e9e9e9"},
{"value": "3", "label": "自然", "itemColor": "#e9e9e9"}
]
}
}
```
> **注意:** 当 `showLabel: true` 时,选项需要同时有 `value`(存储值)和 `label`(显示文本)。
> 当 `showLabel: false` 时,`value` 既是存储值也是显示文本。
---
## 2. 请假申请 (qing_jia_shen_qing_5qfk)
**场景:** 典型 OA 审批表单
**控件清单12个**
- 姓名 (select-user) — 必填,`defaultLogin: true`
- 所在部门 (select-depart)
- 申请日期 (date)
- 请假类型 (select) — 选项:事假/病假/年假/调休
- 开始日期 (date)
- 结束日期 (date)
- 天数 (number)
- 请假说明 (textarea)
- 审批意见 (radio) — 选项:同意/不同意
- 直属领导 (select-user)
- 审批时间 (date)
- 附件 (file-upload)
**config**
```json
{
"titleField": "select_user_1692952011928_137220",
"hasWidgets": ["select-user", "select-depart", "date", "card", "select", "number", "textarea", "radio", "file-upload"]
}
```
> **要点:** `titleField` 指向 select-user 控件(而非 input说明 titleField 可以指向任何控件类型。
---
## 3. 员工基本信息 (yuan_gong_ji_ben_xin_xi_dnjq)
**场景:** HR 员工档案,展示半行布局
**控件清单8个全部半行两两配对**
- [半行] 姓名 (select-user) | 所在部门 (select-depart)
- [半行] 岗位 (input) | 性别 (select) — 选项:男/女
- [半行] 入职时间 (date) | 参加工作时间 (date)
- [半行] 直属上级 (select-user) | 负责 HR (select-user)
**config**
```json
{
"titleField": "select_user_1692874017319_686764",
"hasWidgets": ["select-user", "select-depart", "card", "input", "select", "date"]
}
```
> **要点:** 整个表单全部使用半行布局(每个 card 内 2 个控件autoWidth: 50
---
## 4. 用车申请 (yong_che_shen_qing_gh3j)
**场景:** 行政用车,展示 area-linkage + formula + link-record + divider
**控件清单21个**
- 申请日期 (date) — 必填
- 申请人 (select-user) — 必填
- 申请部门 (select-depart)
- 用车人数 (integer) — 必填
- 要求用车时间 (date)
- 出发地 (area-linkage)
- 出发地详细地址 (input)
- 目的地 (area-linkage)
- 目的地详细地址 (input)
- 随行司机 (select-user)
- 用车理由 (textarea)
- **分隔符 (divider)**
- 出发时间 (date)
- 返程时间 (date)
- 车牌号码 (link-record) — 关联记录
- 起始公里数 (number)
- 到达公里数 (number)
- **行驶公里数 (formula)** — 公式计算
- 停车费 (money)
- 备注 (textarea)
- 附件 (file-upload)
**config**
```json
{
"hasWidgets": ["date", "select-user", "select-depart", "integer", "card", "area-linkage", "input", "textarea", "divider", "link-record", "number", "formula", "money", "file-upload"]
}
```
> **要点:**
> - 使用 `divider` 分隔"申请信息"和"用车记录"两个区域
> - `formula` 控件自动计算行驶公里数
> - `link-record` 关联车辆信息表
---
## 5. 工资表 (gong_zi_biao_zitx)
**场景:** HR 薪资管理,展示 divider 分区 + formula 计算 + money 字段
**控件清单17个**
- 工资发放时间 (date)
- **分隔符 (divider)** — 基本信息区
- 姓名 (select-user) — 必填
- 部门 (select-depart)
- 手机号码 (phone)
- **分隔符 (divider)** — 收入区
- 基本工资 (money)
- 加班工资 (money)
- 奖金 (money)
- 补贴 (money)
- **分隔符 (divider)** — 扣款区
- 本期扣款 (money)
- 五险一金扣款 (money)
- 个税扣除 (money)
- **实发金额 (formula)** — 公式自动计算
- 备注 (textarea)
- 附件 (file-upload)
**config**
```json
{
"hasWidgets": ["date", "card", "divider", "select-user", "select-depart", "phone", "money", "formula", "textarea", "file-upload"]
}
```
> **要点:**
> - 用多个 `divider` 将表单分为"基本信息"、"收入"、"扣款"三个区域
> - `formula` 控件计算实发金额 = 基本工资+加班+奖金+补贴-扣款-五险一金-个税
> - 大量使用 `money` 控件(带"元"后缀)
---
## 6. 会议预约 (hui_yi_yu_yue_0s2h)
**场景:** 行政会议管理,展示 link-record + link-field 关联
**控件清单14个**
- 预约人 (select-user) — 必填
- 所属部门 (select-depart) — 必填
- 当前时间 (date) — 必填
- 会议名称 (input) — 必填
- 会议室基础表 (link-record) — 关联记录
- 会议室名称 (link-field) — 他表字段(自动填充)
- 会议室编号 (link-field)
- 会议室容纳人数 (link-field)
- 参会人员 (select-user) — `multiple: true`(多选)
- 会议开始时间 (date)
- 会议结束时间 (date)
- 预约时间 (input)
- 备注 (textarea)
- 附件 (file-upload)
**config**
```json
{
"hasWidgets": ["select-user", "select-depart", "date", "card", "input", "link-record", "link-field", "textarea", "file-upload"]
}
```
> **要点:**
> - `link-record` 选择会议室后,`link-field` 自动填充关联数据(名称、编号、容纳人数)
> - `select-user` 支持 `multiple: true` 多选参会人员
---
## 7. 地图/定位/省市联动综合 (ceshi_ditu)
**场景:** 展示地图、表字典、下拉树、多选用户/部门
**控件清单9个**
- 用户组件 (select-user) — `multiple: true`
- 部门组件 (select-depart) — `multiple: true`
- 表字典_popupOL报表 (table-dict)
- 表字典_模糊OL报表 (table-dict)
- 表字典_模糊表 (table-dict)
- 下拉树_分类字典 (select-tree)
- 下拉树_表 (select-tree)
- 性别 (radio) — `remote: "dict", dictCode: "sex"`
- **地图 (map)** — 不需要 card 容器
> **要点:** `map` 控件直接放在顶层 list不需要 card 容器。
---
## 常用字典编码速查
从真实表单中收集到的字典编码:
| dictCode | 说明 | 使用场景 |
|----------|------|---------|
| `sex` | 性别 | radio/checkbox/select |
| `position_rank` | 职级 | select 多选 |
| `urgent_level` | 紧急程度 | select 多选 |
---
## 设计模式总结
| 模式 | 说明 | 示例表单 |
|------|------|---------|
| **字典数据源** | `remote:"dict"` + `dictCode` | 字典翻译示例 |
| **半行布局** | card 内两控件 `autoWidth:50` | 员工基本信息 |
| **分区分隔** | divider 控件分割表单区域 | 工资表、用车申请 |
| **公式计算** | formula 控件自动计算 | 工资表、用车申请 |
| **关联填充** | link-record + link-field | 会议预约 |
| **默认当前用户** | select-user `defaultLogin:true` | 请假申请 |
| **多选人员** | select-user `multiple:true` | 会议预约、字典示例 |
| **titleField 灵活** | 可指向 select-user 等非 input 控件 | 请假申请、员工信息 |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,261 @@
"""
JeecgBoot 设计器表单通用创建脚本
=================================
通过 JSON 配置文件创建/更新设计器表单,避免每次编写大量 Python 代码。
用法:
python desform_creator.py --api-base <URL> --token <TOKEN> --config <config.json>
python desform_creator.py --api-base <URL> --token <TOKEN> --config <config.json> --force
参数:
--api-base JeecgBoot 后端地址
--token X-Access-Token
--config JSON 配置文件路径
--force 强制覆盖已存在的表单(默认检测到已存在时退出)
JSON 配置格式:
{
"formName": "工程竣工验收申请表",
"formCode": "eng_completion_acceptance",
"layout": "word", // auto|half|full|word默认 auto
"titleIndex": 0, // 标题字段索引,默认 0第一个非分隔符字段
"fields": [
{"name": "自动编号", "type": "auto-number", "prefix": "GCYS"},
{"name": "条码", "type": "barcode"},
{"name": "工程名称", "type": "input", "required": true},
{"name": "工程类别", "type": "radio", "options": ["土建", "安装"]},
{"name": "开工时间", "type": "date"},
{"name": "工程量清单", "type": "textarea"},
{"name": "图片上传", "type": "imgupload"},
{"name": "定位", "type": "location"},
{"name": "签字", "type": "hand-sign"},
{"name": "---", "type": "divider", "text": "第二部分"},
{"name": "金额", "type": "money", "unit": "万元"},
{"name": "状态", "type": "select", "options": ["启用", "禁用"]},
{"name": "性别", "type": "radio", "dictCode": "sex",
"options": [{"value": "1", "label": ""}, {"value": "2", "label": ""}]}
],
"menuParent": "工程管理", // 可选,生成菜单 SQL 的父菜单名称
"menuIcon": "ant-design:tool-outlined" // 可选,父菜单图标
}
支持的 type 值:
基础: input, textarea, number, integer, money, date, time, switch, slider, rate, color
选择: radio, select, checkbox支持 options + dictCode
系统: select-user, select-depart, phone, email, area-linkage
文件: file-upload, imgupload, hand-sign
高级: auto-number, formula, barcode, location, link-record, link-field
布局: divider, editor, markdown
"""
import argparse
import json
import sys
import os
# 修复 Windows 控制台中文乱码
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
# 自动定位 desform_utils.py
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
_SKILL_DIR = os.path.dirname(_SCRIPT_DIR)
for _path in [os.getcwd(), _SCRIPT_DIR]:
if os.path.exists(os.path.join(_path, 'desform_utils.py')):
sys.path.insert(0, _path)
break
from desform_utils import *
# ============================================================
# desform_utils 未内置的控件
# ============================================================
def _BARCODE(name, width=100, **kw):
"""条码控件"""
code_type = kw.pop('codeType', 'barcode')
w, k, m = make_widget("barcode", name, "form-barcode", "icon-tiaoma", {
"maxWidth": 180, "codeType": code_type, "sourceModel": "",
"hidden": False, "hiddenOnAdd": False, "fieldNote": "", "autoWidth": width
})
return make_card(w), k, m
def _LOCATION(name, required=False, width=100, **kw):
"""定位控件"""
w, k, m = make_widget("location", name, "form-location", "icon-location", {
"width": "100%", "defaultValue": "", "defaultCurrent": False,
"showLngLat": False, "showMap": False, "disabled": False,
"hidden": False, "hiddenOnAdd": False, "required": required,
"fieldNote": "", "autoWidth": width
}, required=required)
return make_card(w), k, m
# ============================================================
# type → 工厂函数 映射
# ============================================================
_TYPE_MAP = {
# 基础
'input': INPUT,
'textarea': TEXTAREA,
'number': NUMBER,
'integer': INTEGER,
'money': MONEY,
'date': DATE,
'time': TIME,
'switch': SWITCH,
'slider': SLIDER,
'rate': RATE,
'color': COLOR,
# 选择
'radio': RADIO,
'select': SELECT,
'checkbox': CHECKBOX,
# 系统
'select-user': USER,
'select-depart': DEPART,
'phone': PHONE,
'email': EMAIL,
'area-linkage': AREA,
# 文件
'file-upload': FILE,
'imgupload': IMGUPLOAD,
'hand-sign': HANDSIGN,
# 高级
'auto-number': AUTONUMBER,
'formula': FORMULA,
'barcode': _BARCODE,
'location': _LOCATION,
'link-record': LINK_RECORD,
'link-field': LINK_FIELD,
# 布局
'divider': DIVIDER,
'editor': EDITOR,
'markdown': MARKDOWN,
}
# 需要 options 参数的控件类型options 作为第二个位置参数)
_OPTION_TYPES = {'radio', 'select', 'checkbox'}
# 参数名映射JSON key → 函数参数名
_PARAM_MAP = {
'required': 'required',
'width': 'width',
'prefix': 'prefix',
'unit': 'unit',
'placeholder': 'placeholder',
'multiple': 'multiple',
'dictCode': 'dict_code',
'unique': 'unique',
'precision': 'precision',
'fmt': 'fmt',
'codeType': 'codeType',
# formula
'mode': 'mode',
'expression': 'expression',
'decimal': 'decimal',
# link-record
'sourceCode': 'source_code',
'titleField': 'title_field',
'showFields': 'show_fields',
'showMode': 'show_mode',
'showType': 'show_type',
# link-field
'linkRecordKey': 'link_record_key',
'showField': 'show_field',
'fieldType': 'field_type',
'fieldOptions': 'field_options',
}
def build_widget(field_def):
"""根据 JSON 字段定义构建控件 tuple"""
ftype = field_def['type']
name = field_def['name']
factory = _TYPE_MAP.get(ftype)
if not factory:
raise ValueError(f'未知的控件类型: {ftype}')
# 构建关键字参数
kwargs = {}
for json_key, param_name in _PARAM_MAP.items():
if json_key in field_def:
kwargs[param_name] = field_def[json_key]
# divider 特殊处理text 参数
if ftype == 'divider':
text = field_def.get('text', name)
return factory(text)
# 需要 options 的控件
if ftype in _OPTION_TYPES:
options = field_def.get('options', [])
return factory(name, options, **kwargs)
# auto-number 特殊处理
if ftype == 'auto-number':
return factory(name, **kwargs)
# 其余控件name + kwargs
return factory(name, **kwargs)
def main():
parser = argparse.ArgumentParser(description='JeecgBoot 设计器表单通用创建工具')
parser.add_argument('--api-base', required=True, help='JeecgBoot 后端地址')
parser.add_argument('--token', required=True, help='X-Access-Token')
parser.add_argument('--config', required=True, help='JSON 配置文件路径')
parser.add_argument('--force', action='store_true', help='强制覆盖已存在的表单')
args = parser.parse_args()
with open(args.config, 'r', encoding='utf-8') as f:
config = json.load(f)
form_name = config['formName']
form_code = config['formCode']
layout = config.get('layout', 'auto')
title_index = config.get('titleIndex', 0)
init_api(args.api_base, args.token)
# 防覆盖检查
existing_id, _ = get_form_id(form_code)
if existing_id and not args.force:
print(f'[阻止] 表单 {form_code} 已存在 (ID={existing_id})')
print(f'如需覆盖,请加 --force 参数')
sys.exit(1)
# 构建控件列表
fields = config.get('fields', [])
widgets = []
for fd in fields:
widget = build_widget(fd)
widgets.append(widget)
# 创建表单
form_id, title_model = create_form(form_name, form_code, widgets,
title_index=title_index, layout=layout)
print(f'\n{"=" * 50}')
print(f'表单创建成功')
print(f'{"=" * 50}')
print(f' 表单ID: {form_id}')
print(f' 表单名称: {form_name}')
print(f' 表单编码: {form_code}')
print(f' 标题字段: {title_model}')
print(f' 布局风格: {layout}')
# 生成菜单 SQL
menu_parent = config.get('menuParent')
if menu_parent:
menu_icon = config.get('menuIcon', 'ant-design:appstore-outlined')
sql = gen_menu_sql(menu_parent, [
(form_name, form_code, 1),
], icon=menu_icon)
print(f'\n--- 菜单 SQL ---\n{sql}')
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff