@steedos-labs/plugin-workflow 3.0.39 → 3.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/designer/dist/amis-renderer/amis-renderer.css +1 -1
- package/designer/dist/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/index-BIbXABqz.css +1 -0
- package/designer/dist/assets/index-DRUi3eGk.js +943 -0
- package/designer/dist/index.html +2 -2
- package/main/default/manager/instance_number_rules.js +1 -1
- package/main/default/manager/uuflow_manager.js +20 -2
- package/main/default/objects/instance_tasks/listviews/inbox.listview.yml +1 -1
- package/main/default/objects/instance_tasks/listviews/outbox.listview.yml +1 -1
- package/main/default/objects/instances/listviews/completed.listview.yml +1 -1
- package/main/default/objects/instances/listviews/draft.listview.yml +1 -1
- package/main/default/objects/instances/listviews/monitor.listview.yml +1 -1
- package/main/default/objects/instances/listviews/pending.listview.yml +1 -1
- package/main/default/pages/flow_selector.page.amis.json +2 -2
- package/main/default/pages/flow_selector_mobile.page.amis.json +2 -2
- package/main/default/pages/page_instance_print.page.amis.json +11 -1
- package/main/default/routes/am.router.js +3 -1
- package/main/default/routes/api_auto_number.router.js +166 -23
- package/main/default/routes/api_workflow_ai_form_design.router.js +116 -16
- package/main/default/routes/api_workflow_ai_form_design_stream.router.js +115 -17
- package/main/default/routes/api_workflow_box_filter.router.js +2 -2
- package/main/default/services/flows.service.js +20 -24
- package/main/default/services/instance.service.js +6 -4
- package/package.json +1 -1
- package/package.service.js +14 -0
- package/public/amis-renderer/amis-renderer.css +1 -1
- package/public/amis-renderer/amis-renderer.js +1 -1
- package/public/workflow/index.css +10 -3
- package/designer/dist/assets/index-CxYuhf9v.js +0 -757
- package/designer/dist/assets/index-Dve-EwQO.css +0 -1
|
@@ -75,6 +75,7 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
|
|
|
75
75
|
- "org" — 组织(选择单个部门)
|
|
76
76
|
- "orgMulti" — 组织(多选,选择多个部门)
|
|
77
77
|
- "reference" — 对象选择(选择指定对象的记录,支持多选,可展示记录的多个字段)
|
|
78
|
+
- "richtext" — 富文本(支持格式化文本、颜色、链接等)
|
|
78
79
|
- required: 布尔值,是否必填
|
|
79
80
|
- colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
|
|
80
81
|
- description: 字符串,字段说明/提示文字,支持 HTML
|
|
@@ -168,6 +169,19 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
|
|
|
168
169
|
- pickerMultiple: 布尔值,是否允许多选,默认 false
|
|
169
170
|
- lookupFilters: 字符串,OData 过滤表达式,如 "status eq 'active'"
|
|
170
171
|
- lookupDisplayFields: 数组,关联数据后额外展示的字段,格式 [{ "field": "字段API名", "label": "显示标签" }]
|
|
172
|
+
- lookupFillRules: 数组,填充规则,选择关联记录后将源字段值自动填充到表单其他字段。格式 [{ "sourceField": "关联对象的字段API名", "targetField": "当前表单中的目标字段name" }]
|
|
173
|
+
- 示例:选择合同后自动填充合同金额到表单的金额字段:lookupFillRules: [{ "sourceField": "amount", "targetField": "contract_amount" }]
|
|
174
|
+
|
|
175
|
+
- member 类型(成员单选)填充规则:
|
|
176
|
+
- pickerFillRules: 数组,选择人员后将人员字段值自动填充到表单其他字段。源对象为 space_users。格式 [{ "sourceField": "源字段API名", "targetField": "当前表单中的目标字段name" }]
|
|
177
|
+
- sourceField 支持点号表示法,可访问关联对象的子属性。成员的 organization 字段引用了 organizations 对象,因此可以使用 "organization.xxx" 访问部门属性
|
|
178
|
+
- 可用的 organization 子属性示例:organization.name(部门名称)、organization.fullname(部门全称)等
|
|
179
|
+
- 示例:选择成员后自动填充其所属部门名称到文本字段:
|
|
180
|
+
pickerFillRules: [{ "sourceField": "organization.name", "targetField": "dept_name" }, { "sourceField": "name", "targetField": "user_name" }]
|
|
181
|
+
|
|
182
|
+
- org 类型(部门单选)填充规则:
|
|
183
|
+
- pickerFillRules: 数组,选择部门后将部门字段值自动填充到表单其他字段。源对象为 organizations。格式同 member 的 pickerFillRules
|
|
184
|
+
- 示例:选择部门后自动填充部门全称到文本字段:pickerFillRules: [{ "sourceField": "fullname", "targetField": "org_fullname" }]
|
|
171
185
|
|
|
172
186
|
- reference 类型(对象选择):
|
|
173
187
|
- reference_to: 字符串,关联对象的 API 名称,如 "contracts"、"accounts"
|
|
@@ -193,16 +207,22 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
|
|
|
193
207
|
## 事件脚本
|
|
194
208
|
|
|
195
209
|
events 对象包含三个可选的 JavaScript 脚本:
|
|
196
|
-
- onInit: 表单初始化时执行。可用参数:form (表单实例), fields (字段数据)
|
|
210
|
+
- onInit: 表单初始化时执行。可用参数:form (表单实例), fields (字段数据), currentStep (当前审批步骤对象)
|
|
197
211
|
- form.setFieldValue(fieldName, value) — 设置字段值
|
|
198
212
|
- form.getFieldValue(fieldName) — 获取字段值
|
|
199
213
|
- form.setFieldOptions(fieldName, options) — 设置下拉选项
|
|
200
214
|
- form.setFieldHidden(fieldName, hidden) — 显隐字段
|
|
201
215
|
- form.setFieldRequired(fieldName, required) — 设为必填
|
|
202
|
-
-
|
|
203
|
-
-
|
|
216
|
+
- form.currentStep — 当前审批步骤对象,包含 name(步骤名)、step_type(步骤类型: start/submit/sign/counterSign/condition)、_id 等属性
|
|
217
|
+
- onValueChange: 字段值变化时执行。可用参数:field (变化的字段), value (新值), oldValue (旧值), form (表单实例), values (所有字段值), currentStep (当前审批步骤对象)
|
|
218
|
+
- onSubmit: 表单提交前执行。可用参数:form (表单实例), values (提交数据), currentStep (当前审批步骤对象)。返回 false 可阻止提交
|
|
219
|
+
|
|
220
|
+
所有脚本支持 async/await。
|
|
221
|
+
⚠️ 脚本内容直接写代码体,参数(form、field、value 等)由运行时自动注入,**禁止用 function(...){} 包裹**。
|
|
222
|
+
正确:"if (field.name === 'x') { ... }"
|
|
223
|
+
错误:"function(field, value, oldValue, form, values) { ... }"
|
|
204
224
|
|
|
205
|
-
|
|
225
|
+
示例:
|
|
206
226
|
\`\`\`javascript
|
|
207
227
|
// onValueChange 示例
|
|
208
228
|
if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
@@ -227,14 +247,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
227
247
|
新版表单已**不再支持** instance_template(审批单模板)、form_script(表单脚本)、flow_events(流程事件),这些是旧版的遗留功能。当用户提供这些数据时,你需要:
|
|
228
248
|
|
|
229
249
|
1. **分析其中的业务逻辑**,理解它们实现了什么功能
|
|
230
|
-
2.
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
250
|
+
2. **⭐ 声明式字段配置优先,脚本兜底**。迁移旧版脚本时,按以下优先级选择实现方式——能用字段属性声明式解决的,绝不写事件脚本:
|
|
251
|
+
- 数值范围校验(如金额上限/下限)→ number 字段的 **min / max** 属性(而非 onSubmit 脚本校验)
|
|
252
|
+
- 字段显隐控制 → 字段的 **visibilityRules**(而非 onValueChange 中 form.setFieldHidden())
|
|
253
|
+
- 动态必填控制 → 字段的 **requiredRules**(而非 onValueChange 中 form.setFieldRequired())
|
|
254
|
+
- 字段值自动计算 → **formula** 类型字段(而非 onValueChange 中 form.setFieldValue()),仅当公式语法无法表达时才退化到 onValueChange
|
|
255
|
+
- 跨对象属性引用(如旧公式 {applicant.organization.name})→ 成员字段的 **pickerFillRules**,sourceField 使用点号表示法如 "organization.name",targetField 指向目标文本字段
|
|
256
|
+
- 字段选项设置 → 字段的 **options** 属性(静态选项),仅动态/异步加载选项时才用 events.onInit / events.onValueChange 中 form.setFieldOptions()
|
|
234
257
|
- 初始化赋值/选项加载 → 写入 events.onInit
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
- 字段选项设置 → 转为字段的 options 属性,或通过 events.onInit / events.onValueChange 中 form.setFieldOptions() 设置
|
|
258
|
+
- 值联动/级联逻辑(无法用 formula 表达时)→ 写入 events.onValueChange
|
|
259
|
+
- 提交前复杂校验(无法用 min/max/required 等表达时)→ 写入 events.onSubmit
|
|
238
260
|
3. **instance_template 仍可用于推断字段 label、colspan(列宽)和 rowspan(行高)**
|
|
239
261
|
4. **不要在输出中返回** instance_template、form_script、flow_events 字段
|
|
240
262
|
|
|
@@ -313,8 +335,54 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
313
335
|
- 新版事件脚本使用 form.getFieldValue() / form.setFieldValue() / form.setFieldOptions() / form.setFieldHidden() / form.setFieldRequired() 等 API
|
|
314
336
|
|
|
315
337
|
### 流程事件 (flow_events)
|
|
316
|
-
-
|
|
317
|
-
-
|
|
338
|
+
- 旧版流程级别的事件脚本,通常绑定在 jQuery 事件上(如 \`instance-before-submit\`、\`instance-before-save\` 等)
|
|
339
|
+
- 分析其中的业务逻辑,**优先用字段声明式配置实现,无法声明式表达时才写入 events 脚本**
|
|
340
|
+
- 常见迁移模式:
|
|
341
|
+
- \`instance-before-submit\` 中的数值范围校验(如 \`money >= 5000000\` 则阻止提交)→ **直接设置 number 字段的 max 属性**,如 \`"max": 4999999.99\`,无需写 onSubmit 脚本
|
|
342
|
+
- \`instance-before-submit\` 中的必填校验 → 设置字段 \`required: true\` 或 \`requiredRules\`
|
|
343
|
+
- \`instance-before-submit\` 中的复杂跨字段校验(如"结束日期必须大于开始日期"且无法用单字段属性表达)→ 写入 events.onSubmit,用 \`form.getFieldValue()\` 获取值,返回 \`false\` 阻止提交
|
|
344
|
+
- \`instance-before-save\` 中的自动赋值 → 写入 events.onInit 或 events.onValueChange
|
|
345
|
+
- \`WorkflowManager.getInstance().state\` 等实例状态判断 → 新版 onSubmit 仅在提交时触发,草稿保存不触发,通常可省略状态判断
|
|
346
|
+
- \`e.preventDefault()\` + \`toastr.error(msg)\` → 新版用 \`form.showError(msg); return false;\` 替代
|
|
347
|
+
- 注意:旧版脚本中的字段引用(如 \`formValues['中文字段名']\`)需替换为新版英文字段 name
|
|
348
|
+
- 注意:旧版脚本是 JavaScript 语法,其中被注释掉的代码(// 单行注释 或 /* */ 块注释)属于废弃逻辑,不需要识别和迁移,直接忽略即可
|
|
349
|
+
- 迁移示例:
|
|
350
|
+
旧版 flow_events:
|
|
351
|
+
\`\`\`javascript
|
|
352
|
+
var money = parseFloat(formValues['申请增加额度']);
|
|
353
|
+
if (money >= 5000000) { e.preventDefault(); toastr.error('金额不能超过500万'); }
|
|
354
|
+
\`\`\`
|
|
355
|
+
新版迁移方式:**不写脚本**,直接在字段上配置:
|
|
356
|
+
\`\`\`json
|
|
357
|
+
{ "name": "apply_increase_amount", "type": "number", "max": 4999999.99, "precision": 2 }
|
|
358
|
+
\`\`\`
|
|
359
|
+
|
|
360
|
+
### 旧版 HTML 字段类型迁移
|
|
361
|
+
- 旧版表单中存在 type 为 \"html\" 的字段,这是旧版的 HTML 编辑器字段,允许用户输入和编辑 HTML 格式的富文本内容
|
|
362
|
+
- 新版表单中 **不再支持 \"html\" 类型**,已被 **\"richtext\"(富文本)** 类型完全替代
|
|
363
|
+
- **迁移规则**:当遇到 type 为 \"html\" 的字段时,必须将其 type 改为 \"richtext\",其余属性(name、label、required、colspan、defaultValue 等)保持不变
|
|
364
|
+
- richtext 类型支持格式化文本、颜色、链接等富文本编辑功能,功能上完全覆盖旧版 html 类型
|
|
365
|
+
- 迁移示例:
|
|
366
|
+
旧版字段:{ "name": "content", "label": "内容", "type": "html" }
|
|
367
|
+
新版字段:{ "name": "content", "label": "内容", "type": "richtext" }
|
|
368
|
+
- migrationLog 记录格式:"字段 <name>: type 从 html 转换为 richtext"
|
|
369
|
+
|
|
370
|
+
### 旧公式字段跨对象引用迁移
|
|
371
|
+
- 旧版表单中,公式字段可能通过 \`{applicant.organization.name}\`、\`{applicant.name}\` 等语法引用成员的关联属性。这种跨对象引用在新版中不再通过公式实现,而是通过**成员字段的 pickerFillRules(填充规则)**来替代。
|
|
372
|
+
- 迁移方式:
|
|
373
|
+
1. 将引用跨对象属性的公式字段改为**普通文本字段**(type: "text")作为目标字段
|
|
374
|
+
2. 在成员字段(type: "member")上配置 **pickerFillRules**,使用点号表示法的 sourceField(如 "organization.name"),targetField 指向该文本字段
|
|
375
|
+
3. 如果 targetField 字段应为只读展示(即旧版是纯展示的公式字段),设置 readonly: true
|
|
376
|
+
- 迁移示例:
|
|
377
|
+
旧版公式字段:\`{applicant.organization.name}\` 用于显示申请人所属部门名称
|
|
378
|
+
新版迁移方式:
|
|
379
|
+
1. 创建文本字段 { "name": "dept_name", "label": "所属部门", "type": "text", "readonly": true }
|
|
380
|
+
2. 在成员字段上配置 pickerFillRules: [{ "sourceField": "organization.name", "targetField": "dept_name" }]
|
|
381
|
+
- 常见映射:
|
|
382
|
+
- \`{applicant.organization.name}\` → sourceField: "organization.name"
|
|
383
|
+
- \`{applicant.organization.fullname}\` → sourceField: "organization.fullname"
|
|
384
|
+
- \`{applicant.name}\` → sourceField: "name"
|
|
385
|
+
- \`{applicant.mobile}\` → sourceField: "mobile"
|
|
318
386
|
|
|
319
387
|
## 输出格式
|
|
320
388
|
返回一个 JSON 对象,格式如下(不要有任何其他文字、解释、markdown 标记):
|
|
@@ -325,8 +393,32 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
325
393
|
"onInit": "脚本代码或空字符串",
|
|
326
394
|
"onValueChange": "脚本代码或空字符串",
|
|
327
395
|
"onSubmit": "脚本代码或空字符串"
|
|
328
|
-
}
|
|
329
|
-
|
|
396
|
+
},
|
|
397
|
+
"migrationLog": ["迁移记录,仅升级场景需要"]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
### migrationLog(迁移记录)
|
|
401
|
+
当用户提供了 flow_events、form_script、instance_template 中包含业务逻辑(如校验、赋值、显隐控制等脚本代码)时,必须在 migrationLog 数组中为每条被迁移的逻辑记录一条说明,格式:
|
|
402
|
+
"<来源>: <原逻辑简述> → <迁移到的具体位置>"
|
|
403
|
+
|
|
404
|
+
迁移目标可以是以下任意一种:
|
|
405
|
+
- 字段属性:如 "字段 apply_increase_amount 的 max:4999999.99"
|
|
406
|
+
- 字段规则:如 "字段 remark 的 visibilityRules"、"字段 attachment_desc 的 requiredRules"
|
|
407
|
+
- formula 字段:如 "新增 formula 字段 total_amount"
|
|
408
|
+
- 事件脚本:如 "events.onSubmit"、"events.onValueChange"
|
|
409
|
+
- 已省略(含原因):如 "已省略(旧版 state=='draft' 判断,新版 onSubmit 仅提交时触发,无需判断)"
|
|
410
|
+
|
|
411
|
+
示例:
|
|
412
|
+
"migrationLog": [
|
|
413
|
+
"flow_events: 申请增加额度>=500万阻止提交 → 字段 apply_increase_amount 的 max:4999999.99",
|
|
414
|
+
"flow_events: ins.state!='draft' 状态判断 → 已省略(新版 onSubmit 仅提交时触发)",
|
|
415
|
+
"form_script: 根据类型字段显隐备注 → 字段 remark 的 visibilityRules",
|
|
416
|
+
"form_script: 选择部门后级联加载人员选项 → events.onValueChange",
|
|
417
|
+
"instance_template: {applicant.organization.name} 跨对象引用 → 成员字段 applicant 的 pickerFillRules (organization.name → dept_name)"
|
|
418
|
+
]
|
|
419
|
+
|
|
420
|
+
⚠️ 如果 flow_events 或 form_script 不为空但 migrationLog 为空数组,视为错误。每条旧版逻辑都必须有对应的迁移记录,不允许静默丢弃。
|
|
421
|
+
如果没有提供 flow_events、form_script,或它们为空,则 migrationLog 可省略或为空数组。`;
|
|
330
422
|
|
|
331
423
|
// Build current fields description
|
|
332
424
|
const fieldsJson = JSON.stringify(fields || [], null, 2);
|
|
@@ -439,7 +531,7 @@ ${userRequest}
|
|
|
439
531
|
'text', 'textarea', 'number', 'date', 'datetime', 'time',
|
|
440
532
|
'select', 'multiSelect', 'checkbox', 'radio', 'file', 'image',
|
|
441
533
|
'lookup', 'section', 'table', 'grid', 'formula',
|
|
442
|
-
'member', 'memberMulti', 'org', 'orgMulti', 'reference'
|
|
534
|
+
'member', 'memberMulti', 'org', 'orgMulti', 'reference', 'richtext'
|
|
443
535
|
];
|
|
444
536
|
const jsIdentifierRe = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
445
537
|
function sanitizeName(name) {
|
|
@@ -458,6 +550,8 @@ ${userRequest}
|
|
|
458
550
|
if (field.type === 'groupPicker') field.type = field.pickerMultiple ? 'orgMulti' : 'org';
|
|
459
551
|
// Backward compat: map legacy select + pickerMultiple to multiSelect
|
|
460
552
|
if (field.type === 'select' && field.pickerMultiple) field.type = 'multiSelect';
|
|
553
|
+
// Backward compat: map legacy html type to richtext
|
|
554
|
+
if (field.type === 'html') field.type = 'richtext';
|
|
461
555
|
if (field.type && !validTypes.includes(field.type)) {
|
|
462
556
|
field.type = 'text'; // fallback to text for unknown types
|
|
463
557
|
}
|
|
@@ -481,6 +575,8 @@ ${userRequest}
|
|
|
481
575
|
if (child.type === 'userPicker') child.type = child.pickerMultiple ? 'memberMulti' : 'member';
|
|
482
576
|
if (child.type === 'groupPicker') child.type = child.pickerMultiple ? 'orgMulti' : 'org';
|
|
483
577
|
if (child.type === 'select' && child.pickerMultiple) child.type = 'multiSelect';
|
|
578
|
+
// Backward compat: map legacy html type to richtext
|
|
579
|
+
if (child.type === 'html') child.type = 'richtext';
|
|
484
580
|
if (child.type && !validTypes.includes(child.type)) {
|
|
485
581
|
child.type = 'text';
|
|
486
582
|
}
|
|
@@ -512,6 +608,10 @@ ${userRequest}
|
|
|
512
608
|
onSubmit: result.events.onSubmit || '',
|
|
513
609
|
},
|
|
514
610
|
};
|
|
611
|
+
// Pass through migrationLog if present (for upgrade audit trail)
|
|
612
|
+
if (Array.isArray(result.migrationLog) && result.migrationLog.length > 0) {
|
|
613
|
+
responseData.migrationLog = result.migrationLog;
|
|
614
|
+
}
|
|
515
615
|
return res.json(responseData);
|
|
516
616
|
|
|
517
617
|
} catch (error) {
|
|
@@ -302,7 +302,7 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
|
|
|
302
302
|
'text', 'textarea', 'number', 'date', 'datetime', 'time',
|
|
303
303
|
'select', 'multiSelect', 'checkbox', 'radio', 'file', 'image',
|
|
304
304
|
'lookup', 'section', 'table', 'grid', 'formula',
|
|
305
|
-
'member', 'memberMulti', 'org', 'orgMulti', 'reference'
|
|
305
|
+
'member', 'memberMulti', 'org', 'orgMulti', 'reference', 'richtext'
|
|
306
306
|
];
|
|
307
307
|
const jsIdentifierRe = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
308
308
|
function sanitizeName(name) {
|
|
@@ -321,6 +321,8 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
|
|
|
321
321
|
if (field.type === 'groupPicker') field.type = field.pickerMultiple ? 'orgMulti' : 'org';
|
|
322
322
|
// Backward compat: map legacy select + pickerMultiple to multiSelect
|
|
323
323
|
if (field.type === 'select' && field.pickerMultiple) field.type = 'multiSelect';
|
|
324
|
+
// Backward compat: map legacy html type to richtext
|
|
325
|
+
if (field.type === 'html') field.type = 'richtext';
|
|
324
326
|
if (field.type && !validTypes.includes(field.type)) {
|
|
325
327
|
field.type = 'text';
|
|
326
328
|
}
|
|
@@ -342,6 +344,8 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
|
|
|
342
344
|
if (child.type === 'userPicker') child.type = child.pickerMultiple ? 'memberMulti' : 'member';
|
|
343
345
|
if (child.type === 'groupPicker') child.type = child.pickerMultiple ? 'orgMulti' : 'org';
|
|
344
346
|
if (child.type === 'select' && child.pickerMultiple) child.type = 'multiSelect';
|
|
347
|
+
// Backward compat: map legacy html type to richtext
|
|
348
|
+
if (child.type === 'html') child.type = 'richtext';
|
|
345
349
|
if (child.type && !validTypes.includes(child.type)) {
|
|
346
350
|
child.type = 'text';
|
|
347
351
|
}
|
|
@@ -370,6 +374,10 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
|
|
|
370
374
|
onSubmit: result.events.onSubmit || '',
|
|
371
375
|
},
|
|
372
376
|
};
|
|
377
|
+
// Pass through migrationLog if present (for upgrade audit trail)
|
|
378
|
+
if (Array.isArray(result.migrationLog) && result.migrationLog.length > 0) {
|
|
379
|
+
responseData.migrationLog = result.migrationLog;
|
|
380
|
+
}
|
|
373
381
|
|
|
374
382
|
const fieldCount = (responseData.fields || []).length;
|
|
375
383
|
sendEvent('progress', { stage: 'complete', message: `转换完成!共 ${fieldCount} 个字段` });
|
|
@@ -429,6 +437,7 @@ function buildSystemPrompt() {
|
|
|
429
437
|
- "org" — 组织(选择单个部门)
|
|
430
438
|
- "orgMulti" — 组织(多选,选择多个部门)
|
|
431
439
|
- "reference" — 对象选择(选择指定对象的记录,支持多选,可展示记录的多个字段)
|
|
440
|
+
- "richtext" — 富文本(支持格式化文本、颜色、链接等)
|
|
432
441
|
- required: 布尔值,是否必填
|
|
433
442
|
- colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
|
|
434
443
|
- description: 字符串,字段说明/提示文字,支持 HTML
|
|
@@ -522,6 +531,19 @@ function buildSystemPrompt() {
|
|
|
522
531
|
- pickerMultiple: 布尔值,是否允许多选,默认 false
|
|
523
532
|
- lookupFilters: 字符串,OData 过滤表达式,如 "status eq 'active'"
|
|
524
533
|
- lookupDisplayFields: 数组,关联数据后额外展示的字段,格式 [{ "field": "字段API名", "label": "显示标签" }]
|
|
534
|
+
- lookupFillRules: 数组,填充规则,选择关联记录后将源字段值自动填充到表单其他字段。格式 [{ "sourceField": "关联对象的字段API名", "targetField": "当前表单中的目标字段name" }]
|
|
535
|
+
- 示例:选择合同后自动填充合同金额到表单的金额字段:lookupFillRules: [{ "sourceField": "amount", "targetField": "contract_amount" }]
|
|
536
|
+
|
|
537
|
+
- member 类型(成员单选)填充规则:
|
|
538
|
+
- pickerFillRules: 数组,选择人员后将人员字段值自动填充到表单其他字段。源对象为 space_users。格式 [{ "sourceField": "源字段API名", "targetField": "当前表单中的目标字段name" }]
|
|
539
|
+
- sourceField 支持点号表示法,可访问关联对象的子属性。成员的 organization 字段引用了 organizations 对象,因此可以使用 "organization.xxx" 访问部门属性
|
|
540
|
+
- 可用的 organization 子属性示例:organization.name(部门名称)、organization.fullname(部门全称)等
|
|
541
|
+
- 示例:选择成员后自动填充其所属部门名称到文本字段:
|
|
542
|
+
pickerFillRules: [{ "sourceField": "organization.name", "targetField": "dept_name" }, { "sourceField": "name", "targetField": "user_name" }]
|
|
543
|
+
|
|
544
|
+
- org 类型(部门单选)填充规则:
|
|
545
|
+
- pickerFillRules: 数组,选择部门后将部门字段值自动填充到表单其他字段。源对象为 organizations。格式同 member 的 pickerFillRules
|
|
546
|
+
- 示例:选择部门后自动填充部门全称到文本字段:pickerFillRules: [{ "sourceField": "fullname", "targetField": "org_fullname" }]
|
|
525
547
|
|
|
526
548
|
- reference 类型(对象选择):
|
|
527
549
|
- reference_to: 字符串,关联对象的 API 名称,如 "contracts"、"accounts"
|
|
@@ -547,16 +569,22 @@ function buildSystemPrompt() {
|
|
|
547
569
|
## 事件脚本
|
|
548
570
|
|
|
549
571
|
events 对象包含三个可选的 JavaScript 脚本:
|
|
550
|
-
- onInit: 表单初始化时执行。可用参数:form (表单实例), fields (字段数据)
|
|
572
|
+
- onInit: 表单初始化时执行。可用参数:form (表单实例), fields (字段数据), currentStep (当前审批步骤对象)
|
|
551
573
|
- form.setFieldValue(fieldName, value) — 设置字段值
|
|
552
574
|
- form.getFieldValue(fieldName) — 获取字段值
|
|
553
575
|
- form.setFieldOptions(fieldName, options) — 设置下拉选项
|
|
554
576
|
- form.setFieldHidden(fieldName, hidden) — 显隐字段
|
|
555
577
|
- form.setFieldRequired(fieldName, required) — 设为必填
|
|
556
|
-
-
|
|
557
|
-
-
|
|
578
|
+
- form.currentStep — 当前审批步骤对象,包含 name(步骤名)、step_type(步骤类型: start/submit/sign/counterSign/condition)、_id 等属性
|
|
579
|
+
- onValueChange: 字段值变化时执行。可用参数:field (变化的字段), value (新值), oldValue (旧值), form (表单实例), values (所有字段值), currentStep (当前审批步骤对象)
|
|
580
|
+
- onSubmit: 表单提交前执行。可用参数:form (表单实例), values (提交数据), currentStep (当前审批步骤对象)。返回 false 可阻止提交
|
|
558
581
|
|
|
559
|
-
所有脚本支持 async/await
|
|
582
|
+
所有脚本支持 async/await。
|
|
583
|
+
⚠️ 脚本内容直接写代码体,参数(form、field、value 等)由运行时自动注入,**禁止用 function(...){} 包裹**。
|
|
584
|
+
正确:"if (field.name === 'x') { ... }"
|
|
585
|
+
错误:"function(field, value, oldValue, form, values) { ... }"
|
|
586
|
+
|
|
587
|
+
示例:
|
|
560
588
|
\`\`\`javascript
|
|
561
589
|
// onValueChange 示例
|
|
562
590
|
if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
@@ -581,14 +609,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
581
609
|
新版表单已**不再支持** instance_template(审批单模板)、form_script(表单脚本)、flow_events(流程事件),这些是旧版的遗留功能。当用户提供这些数据时,你需要:
|
|
582
610
|
|
|
583
611
|
1. **分析其中的业务逻辑**,理解它们实现了什么功能
|
|
584
|
-
2.
|
|
585
|
-
-
|
|
586
|
-
-
|
|
587
|
-
-
|
|
612
|
+
2. **⭐ 声明式字段配置优先,脚本兜底**。迁移旧版脚本时,按以下优先级选择实现方式——能用字段属性声明式解决的,绝不写事件脚本:
|
|
613
|
+
- 数值范围校验(如金额上限/下限)→ number 字段的 **min / max** 属性(而非 onSubmit 脚本校验)
|
|
614
|
+
- 字段显隐控制 → 字段的 **visibilityRules**(而非 onValueChange 中 form.setFieldHidden())
|
|
615
|
+
- 动态必填控制 → 字段的 **requiredRules**(而非 onValueChange 中 form.setFieldRequired())
|
|
616
|
+
- 字段值自动计算 → **formula** 类型字段(而非 onValueChange 中 form.setFieldValue()),仅当公式语法无法表达时才退化到 onValueChange
|
|
617
|
+
- 跨对象属性引用(如旧公式 {applicant.organization.name})→ 成员字段的 **pickerFillRules**,sourceField 使用点号表示法如 "organization.name",targetField 指向目标文本字段
|
|
618
|
+
- 字段选项设置 → 字段的 **options** 属性(静态选项),仅动态/异步加载选项时才用 events.onInit / events.onValueChange 中 form.setFieldOptions()
|
|
588
619
|
- 初始化赋值/选项加载 → 写入 events.onInit
|
|
589
|
-
-
|
|
590
|
-
-
|
|
591
|
-
- 字段选项设置 → 转为字段的 options 属性,或通过 events.onInit / events.onValueChange 中 form.setFieldOptions() 设置
|
|
620
|
+
- 值联动/级联逻辑(无法用 formula 表达时)→ 写入 events.onValueChange
|
|
621
|
+
- 提交前复杂校验(无法用 min/max/required 等表达时)→ 写入 events.onSubmit
|
|
592
622
|
3. **instance_template 仍可用于推断字段 label、colspan(列宽)和 rowspan(行高)**
|
|
593
623
|
4. **不要在输出中返回** instance_template、form_script、flow_events 字段
|
|
594
624
|
|
|
@@ -667,9 +697,53 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
667
697
|
- 新版事件脚本使用 form.getFieldValue() / form.setFieldValue() / form.setFieldOptions() / form.setFieldHidden() / form.setFieldRequired() 等 API
|
|
668
698
|
|
|
669
699
|
### 流程事件 (flow_events)
|
|
670
|
-
-
|
|
671
|
-
-
|
|
672
|
-
|
|
700
|
+
- 旧版流程级别的事件脚本,通常绑定在 jQuery 事件上(如 \`instance-before-submit\`、\`instance-before-save\` 等)
|
|
701
|
+
- 分析其中的业务逻辑,**优先用字段声明式配置实现,无法声明式表达时才写入 events 脚本**
|
|
702
|
+
- 常见迁移模式:
|
|
703
|
+
- \`instance-before-submit\` 中的数值范围校验(如 \`money >= 5000000\` 则阻止提交)→ **直接设置 number 字段的 max 属性**,如 \`"max": 4999999.99\`,无需写 onSubmit 脚本
|
|
704
|
+
- \`instance-before-submit\` 中的必填校验 → 设置字段 \`required: true\` 或 \`requiredRules\`
|
|
705
|
+
- \`instance-before-submit\` 中的复杂跨字段校验(如"结束日期必须大于开始日期"且无法用单字段属性表达)→ 写入 events.onSubmit,用 \`form.getFieldValue()\` 获取值,返回 \`false\` 阻止提交
|
|
706
|
+
- \`instance-before-save\` 中的自动赋值 → 写入 events.onInit 或 events.onValueChange
|
|
707
|
+
- \`WorkflowManager.getInstance().state\` 等实例状态判断 → 新版 onSubmit 仅在提交时触发,草稿保存不触发,通常可省略状态判断
|
|
708
|
+
- \`e.preventDefault()\` + \`toastr.error(msg)\` → 新版用 \`form.showError(msg); return false;\` 替代
|
|
709
|
+
- 注意:旧版脚本中的字段引用(如 \`formValues['中文字段名']\`)需替换为新版英文字段 name
|
|
710
|
+
- 注意:旧版脚本是 JavaScript 语法,其中被注释掉的代码(// 单行注释 或 /* */ 块注释)属于废弃逻辑,不需要识别和迁移,直接忽略即可
|
|
711
|
+
- 迁移示例:
|
|
712
|
+
旧版 flow_events:
|
|
713
|
+
\`\`\`javascript
|
|
714
|
+
var money = parseFloat(formValues['申请增加额度']);
|
|
715
|
+
if (money >= 5000000) { e.preventDefault(); toastr.error('金额不能超过500万'); }
|
|
716
|
+
\`\`\`
|
|
717
|
+
新版迁移方式:**不写脚本**,直接在字段上配置:
|
|
718
|
+
\`\`\`json
|
|
719
|
+
{ "name": "apply_increase_amount", "type": "number", "max": 4999999.99, "precision": 2 }
|
|
720
|
+
\`\`\`
|
|
721
|
+
### 旧版 HTML 字段类型迁移
|
|
722
|
+
- 旧版表单中存在 type 为 \"html\" 的字段,这是旧版的 HTML 编辑器字段,允许用户输入和编辑 HTML 格式的富文本内容
|
|
723
|
+
- 新版表单中 **不再支持 \"html\" 类型**,已被 **\"richtext\"(富文本)** 类型完全替代
|
|
724
|
+
- **迁移规则**:当遇到 type 为 \"html\" 的字段时,必须将其 type 改为 \"richtext\",其余属性(name、label、required、colspan、defaultValue 等)保持不变
|
|
725
|
+
- richtext 类型支持格式化文本、颜色、链接等富文本编辑功能,功能上完全覆盖旧版 html 类型
|
|
726
|
+
- 迁移示例:
|
|
727
|
+
旧版字段:{ "name": "content", "label": "内容", "type": "html" }
|
|
728
|
+
新版字段:{ "name": "content", "label": "内容", "type": "richtext" }
|
|
729
|
+
- migrationLog 记录格式:"字段 <name>: type 从 html 转换为 richtext"
|
|
730
|
+
|
|
731
|
+
### 旧公式字段跨对象引用迁移
|
|
732
|
+
- 旧版表单中,公式字段可能通过 \`{applicant.organization.name}\`、\`{applicant.name}\` 等语法引用成员的关联属性。这种跨对象引用在新版中不再通过公式实现,而是通过**成员字段的 pickerFillRules(填充规则)**来替代。
|
|
733
|
+
- 迁移方式:
|
|
734
|
+
1. 将引用跨对象属性的公式字段改为**普通文本字段**(type: "text")作为目标字段
|
|
735
|
+
2. 在成员字段(type: "member")上配置 **pickerFillRules**,使用点号表示法的 sourceField(如 "organization.name"),targetField 指向该文本字段
|
|
736
|
+
3. 如果 targetField 字段应为只读展示(即旧版是纯展示的公式字段),设置 readonly: true
|
|
737
|
+
- 迁移示例:
|
|
738
|
+
旧版公式字段:\`{applicant.organization.name}\` 用于显示申请人所属部门名称
|
|
739
|
+
新版迁移方式:
|
|
740
|
+
1. 创建文本字段 { "name": "dept_name", "label": "所属部门", "type": "text", "readonly": true }
|
|
741
|
+
2. 在成员字段上配置 pickerFillRules: [{ "sourceField": "organization.name", "targetField": "dept_name" }]
|
|
742
|
+
- 常见映射:
|
|
743
|
+
- \`{applicant.organization.name}\` → sourceField: "organization.name"
|
|
744
|
+
- \`{applicant.organization.fullname}\` → sourceField: "organization.fullname"
|
|
745
|
+
- \`{applicant.name}\` → sourceField: "name"
|
|
746
|
+
- \`{applicant.mobile}\` → sourceField: "mobile"
|
|
673
747
|
## 输出格式
|
|
674
748
|
返回一个 JSON 对象,格式如下(不要有任何其他文字、解释、markdown 标记):
|
|
675
749
|
{
|
|
@@ -679,8 +753,32 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
679
753
|
"onInit": "脚本代码或空字符串",
|
|
680
754
|
"onValueChange": "脚本代码或空字符串",
|
|
681
755
|
"onSubmit": "脚本代码或空字符串"
|
|
682
|
-
}
|
|
683
|
-
|
|
756
|
+
},
|
|
757
|
+
"migrationLog": ["迁移记录,仅升级场景需要"]
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
### migrationLog(迁移记录)
|
|
761
|
+
当用户提供了 flow_events、form_script、instance_template 中包含业务逻辑(如校验、赋值、显隐控制等脚本代码)时,必须在 migrationLog 数组中为每条被迁移的逻辑记录一条说明,格式:
|
|
762
|
+
"<来源>: <原逻辑简述> → <迁移到的具体位置>"
|
|
763
|
+
|
|
764
|
+
迁移目标可以是以下任意一种:
|
|
765
|
+
- 字段属性:如 "字段 apply_increase_amount 的 max:4999999.99"
|
|
766
|
+
- 字段规则:如 "字段 remark 的 visibilityRules"、"字段 attachment_desc 的 requiredRules"
|
|
767
|
+
- formula 字段:如 "新增 formula 字段 total_amount"
|
|
768
|
+
- 事件脚本:如 "events.onSubmit"、"events.onValueChange"
|
|
769
|
+
- 已省略(含原因):如 "已省略(旧版 state=='draft' 判断,新版 onSubmit 仅提交时触发,无需判断)"
|
|
770
|
+
|
|
771
|
+
示例:
|
|
772
|
+
"migrationLog": [
|
|
773
|
+
"flow_events: 申请增加额度>=500万阻止提交 → 字段 apply_increase_amount 的 max:4999999.99",
|
|
774
|
+
"flow_events: ins.state!='draft' 状态判断 → 已省略(新版 onSubmit 仅提交时触发)",
|
|
775
|
+
"form_script: 根据类型字段显隐备注 → 字段 remark 的 visibilityRules",
|
|
776
|
+
"form_script: 选择部门后级联加载人员选项 → events.onValueChange",
|
|
777
|
+
"instance_template: {applicant.organization.name} 跨对象引用 → 成员字段 applicant 的 pickerFillRules (organization.name → dept_name)"
|
|
778
|
+
]
|
|
779
|
+
|
|
780
|
+
⚠️ 如果 flow_events 或 form_script 不为空但 migrationLog 为空数组,视为错误。每条旧版逻辑都必须有对应的迁移记录,不允许静默丢弃。
|
|
781
|
+
如果没有提供 flow_events、form_script,或它们为空,则 migrationLog 可省略或为空数组。`;
|
|
684
782
|
}
|
|
685
783
|
|
|
686
784
|
exports.default = router;
|
|
@@ -14,10 +14,10 @@ router.get('/api/workflow/v2/:box/filter', requireAuthentication, async function
|
|
|
14
14
|
const userSession = req.user;
|
|
15
15
|
const { userId, is_space_admin, spaceId } = userSession;
|
|
16
16
|
// TODO 按应用分类显示
|
|
17
|
-
const { app, flowId } = req.query;
|
|
17
|
+
const { app, flowId, additionalFilters } = req.query;
|
|
18
18
|
const { box } = req.params;
|
|
19
19
|
const filter = await objectql.getSteedosSchema().broker.call("instance.getBoxFilters", {
|
|
20
|
-
box, appId: app, flowId, userId, is_space_admin, spaceId
|
|
20
|
+
box, appId: app, flowId, additionalFilters, userId, is_space_admin, spaceId
|
|
21
21
|
})
|
|
22
22
|
return res.send({
|
|
23
23
|
filter
|
|
@@ -144,37 +144,33 @@ module.exports = {
|
|
|
144
144
|
}
|
|
145
145
|
} else {
|
|
146
146
|
var categories = await this.getSpaceCategories(categoriesIds, userSession);
|
|
147
|
+
const allFilters = [
|
|
148
|
+
["category", "in", _.map(categories, '_id')],
|
|
149
|
+
["state", "=", "enabled"],
|
|
150
|
+
];
|
|
151
|
+
if (action === "new") allFilters.push(["forbid_initiate_instance", "!=", true]);
|
|
152
|
+
if (keywordsFilter) allFilters.push(keywordsFilter);
|
|
153
|
+
const allFlows = await objectql.getObject("flows").find({
|
|
154
|
+
filters: allFilters,
|
|
155
|
+
fields: ['_id', 'name', 'sort_no', 'category', 'perms'],
|
|
156
|
+
});
|
|
157
|
+
const flowsByCategory = _.groupBy(allFlows, 'category');
|
|
147
158
|
for (const category of categories) {
|
|
148
|
-
const
|
|
149
|
-
["category", "=", category._id],
|
|
150
|
-
["state", "=", "enabled"],
|
|
151
|
-
];
|
|
152
|
-
if(action === "new"){
|
|
153
|
-
filters.push(["forbid_initiate_instance", "!=", true]);
|
|
154
|
-
}
|
|
155
|
-
if (keywordsFilter) {
|
|
156
|
-
filters.push(keywordsFilter)
|
|
157
|
-
}
|
|
158
|
-
// console.log(`filters`, filters)
|
|
159
|
-
const categoryFlows = await objectql.getObject("flows").find({
|
|
160
|
-
filters: filters,
|
|
161
|
-
fields: ['_id', 'name', 'sort_no', 'category', 'perms'],
|
|
162
|
-
sort: "sort_no desc,name",
|
|
163
|
-
});
|
|
159
|
+
const catFlows = flowsByCategory[category._id] || [];
|
|
164
160
|
category.flows = [];
|
|
165
|
-
for (const flow of
|
|
166
|
-
if(allData){
|
|
161
|
+
for (const flow of catFlows) {
|
|
162
|
+
if (allData) {
|
|
163
|
+
category.flows.push(flow);
|
|
164
|
+
} else if (this.canAdd(flow, userSession)) {
|
|
167
165
|
category.flows.push(flow);
|
|
168
|
-
}else{
|
|
169
|
-
if (this.
|
|
166
|
+
} else if (action === 'query') {
|
|
167
|
+
if (is_space_admin || this.canMonitor(flow, userSession) || this.canAdmin(flow, userSession)) {
|
|
170
168
|
category.flows.push(flow);
|
|
171
|
-
} else if (action == 'query') {
|
|
172
|
-
if (is_space_admin || this.canMonitor(flow, userSession) || this.canAdmin(flow, userSession)) {
|
|
173
|
-
category.flows.push(flow);
|
|
174
|
-
}
|
|
175
169
|
}
|
|
176
170
|
}
|
|
177
171
|
}
|
|
172
|
+
// Sort per-category in memory to avoid MongoDB large-dataset sort RAM limit
|
|
173
|
+
category.flows = _.orderBy(category.flows, ['sort_no', 'name'], ['desc', 'asc']);
|
|
178
174
|
}
|
|
179
175
|
data = categories;
|
|
180
176
|
}
|
|
@@ -134,8 +134,7 @@ module.exports = {
|
|
|
134
134
|
},
|
|
135
135
|
getBoxFilters: {
|
|
136
136
|
async handler(ctx){
|
|
137
|
-
const { appId, box, flowId, userId, is_space_admin, spaceId } = ctx.params;
|
|
138
|
-
const categoriesIds = await this.getAppCategoriesIds(appId);
|
|
137
|
+
const { appId, box, flowId, userId, is_space_admin, spaceId, additionalFilters } = ctx.params;
|
|
139
138
|
const filter = [];
|
|
140
139
|
switch (box) {
|
|
141
140
|
case 'inbox':
|
|
@@ -182,8 +181,11 @@ module.exports = {
|
|
|
182
181
|
break;
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
if(
|
|
186
|
-
|
|
184
|
+
if(!additionalFilters || additionalFilters.indexOf('category') < 0){
|
|
185
|
+
const categoriesIds = await this.getAppCategoriesIds(appId);
|
|
186
|
+
if(categoriesIds && categoriesIds.length > 0){
|
|
187
|
+
filter.push(['category', 'in', categoriesIds])
|
|
188
|
+
}
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
return filter;
|
package/package.json
CHANGED
package/package.service.js
CHANGED
|
@@ -51,6 +51,20 @@ module.exports = {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
|
+
getUser: {
|
|
55
|
+
rest: {
|
|
56
|
+
method: 'GET',
|
|
57
|
+
fullPath: '/api/workflow/getCurrentUser'
|
|
58
|
+
},
|
|
59
|
+
async handler(ctx) {
|
|
60
|
+
try {
|
|
61
|
+
return ctx.meta.user;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(error);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
54
68
|
send_badge_to_user: {
|
|
55
69
|
params: {
|
|
56
70
|
send_from: { type: 'string', optional: false },
|