@steedos-labs/plugin-workflow 3.0.45 → 3.0.47
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-CH5Rzd1r.css +1 -0
- package/designer/dist/assets/index-Z3wTJs2h.js +952 -0
- package/designer/dist/index.html +2 -2
- package/main/default/manager/handlers_manager.js +4 -4
- package/main/default/manager/import.js +20 -2
- package/main/default/manager/uuflow_manager.js +1 -1
- package/main/default/objects/instances/buttons/instance_delete.button.yml +1 -1
- package/main/default/objects/instances/buttons/instance_related.button.yml +4 -1
- package/main/default/objects/instances/buttons/instance_terminate.button.yml +1 -1
- package/main/default/routes/am.router.js +36 -1
- package/main/default/routes/amis_form_design.router.js +1 -1
- package/main/default/routes/api_workflow_ai_form_design.router.js +43 -24
- package/main/default/routes/api_workflow_ai_form_design_stream.router.js +15 -24
- package/main/default/routes/api_workflow_instance_forward.router.js +2 -2
- package/main/default/routes/api_workflow_next_step_users.router.js +18 -1
- package/main/default/routes/flow_form_design.ejs +13 -1
- package/main/default/routes/flow_form_design.router.js +4 -3
- package/main/default/test/FORMULA_SCAN_GUIDE.md +258 -0
- package/main/default/test/prompt-formula-analyze.md +59 -0
- package/main/default/test/prompt-formula-fix.md +75 -0
- package/main/default/test/reports/formula-scan/.gitkeep +0 -0
- package/main/default/test/reports/formula-scan/SCAN_HISTORY.md +165 -0
- package/main/default/test/reports/formula-scan/scan-result-v6-20260324.json +31846 -0
- package/main/default/test/reports/formula-scan/scan-result-v7-20260324.json +31543 -0
- package/main/default/test/run_approval_comments_upgrade.js +135 -0
- package/main/default/test/scan_production_formulas.js +573 -0
- package/main/default/test/test_formula_compat.js +52 -2
- package/main/default/utils/designerManager.js +2 -1
- package/main/default/utils/formula-compat.js +15 -1
- package/package.json +1 -1
- package/public/amis-renderer/amis-renderer.css +1 -1
- package/public/amis-renderer/amis-renderer.js +1 -1
- package/public/workflow/index.css +4 -0
- package/src/schema/steedos_form_schema.amis.js +3 -1
- package/designer/dist/assets/index-B5zmn1Wt.css +0 -1
- package/designer/dist/assets/index-D7W5yEF4.js +0 -943
package/designer/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="shortcut icon" type="image/svg+xml" href="/images/logo.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>designer</title>
|
|
8
|
-
<script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-Z3wTJs2h.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-CH5Rzd1r.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -285,7 +285,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
285
285
|
let field_code = null;
|
|
286
286
|
|
|
287
287
|
for (const form_field of form_fields) {
|
|
288
|
-
if (form_field._id === approver_org_field) {
|
|
288
|
+
if (form_field._id === approver_org_field || form_field.code === approver_org_field || form_field.name === approver_org_field) {
|
|
289
289
|
field_code = form_field.code;
|
|
290
290
|
break;
|
|
291
291
|
}
|
|
@@ -361,7 +361,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
361
361
|
let field_code = null;
|
|
362
362
|
|
|
363
363
|
for (const form_field of form_fields) {
|
|
364
|
-
if (form_field._id === approver_org_field) {
|
|
364
|
+
if (form_field._id === approver_org_field || form_field.code === approver_org_field || form_field.name === approver_org_field) {
|
|
365
365
|
field_code = form_field.code;
|
|
366
366
|
break;
|
|
367
367
|
}
|
|
@@ -466,7 +466,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
466
466
|
let field_code = null;
|
|
467
467
|
|
|
468
468
|
for (const form_field of form_fields) {
|
|
469
|
-
if (form_field._id === approver_user_field) {
|
|
469
|
+
if (form_field._id === approver_user_field || form_field.code === approver_user_field || form_field.name === approver_user_field) {
|
|
470
470
|
field_code = form_field.code;
|
|
471
471
|
break;
|
|
472
472
|
}
|
|
@@ -546,7 +546,7 @@ getHandlersManager.getHandlers = async function (instance_id, step_id, login_use
|
|
|
546
546
|
let field_code = null;
|
|
547
547
|
|
|
548
548
|
for (const form_field of form_fields) {
|
|
549
|
-
if (form_field._id === approver_user_field) {
|
|
549
|
+
if (form_field._id === approver_user_field || form_field.code === approver_user_field || form_field.name === approver_user_field) {
|
|
550
550
|
field_code = form_field.code;
|
|
551
551
|
break;
|
|
552
552
|
}
|
|
@@ -94,7 +94,7 @@ async function upgradeForm(formId, form, currentUserId, spaceId) {
|
|
|
94
94
|
pass = recordsCount > 0;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
if (pass === true
|
|
97
|
+
if (pass === true) {
|
|
98
98
|
formUpdateObj.$push = { 'historys': ff["current"] };
|
|
99
99
|
current._id = _makeNewID();
|
|
100
100
|
current._rev = ff["current"]["_rev"] + 1;
|
|
@@ -124,6 +124,22 @@ async function upgradeForm(formId, form, currentUserId, spaceId) {
|
|
|
124
124
|
current.form_script = form["current"]["form_script"];
|
|
125
125
|
current.name_forumla = form["current"]["name_forumla"];
|
|
126
126
|
|
|
127
|
+
current.style = form["current"]["style"];
|
|
128
|
+
current.mode = form["current"]["mode"];
|
|
129
|
+
current.wizard_mode = form["current"]["wizard_mode"];
|
|
130
|
+
current.amis_schema = form["current"]["amis_schema"]
|
|
131
|
+
|
|
132
|
+
current.version = form["current"]["version"];
|
|
133
|
+
current.viewMode = form["current"]["viewMode"];
|
|
134
|
+
current.tableColumns = form["current"]["tableColumns"];
|
|
135
|
+
|
|
136
|
+
current.events = form["current"]["events"];
|
|
137
|
+
|
|
138
|
+
current.formTitle = form["current"]["formTitle"];
|
|
139
|
+
current.tableTitleColor = form["current"]["tableTitleColor"];
|
|
140
|
+
current.tableBorderColor = form["current"]["tableBorderColor"];
|
|
141
|
+
current.tableShowOuterBorder = form["current"]["tableShowOuterBorder"];
|
|
142
|
+
|
|
127
143
|
formUpdateObj.$set = {
|
|
128
144
|
'current': current,
|
|
129
145
|
'name': form["name"],
|
|
@@ -232,6 +248,7 @@ async function upgradeFlow(flowCome, userId, flowId) {
|
|
|
232
248
|
updateObj.$set.modified = now;
|
|
233
249
|
updateObj.$set.modified_by = userId;
|
|
234
250
|
updateObj.$set.events = flowCome['events'] || '';
|
|
251
|
+
updateObj.$set.upgraded = flowCome.upgraded || false;
|
|
235
252
|
|
|
236
253
|
const form = await formCollection.findOne({ _id: flow.form }, { projection: { category: 1 } });
|
|
237
254
|
updateObj.$set.category = form['category'];
|
|
@@ -409,9 +426,10 @@ async function workflow(uid, spaceId, form, enabled, company_id, options = {}) {
|
|
|
409
426
|
form.modified_by = uid;
|
|
410
427
|
form.historys = [];
|
|
411
428
|
|
|
412
|
-
const fields = addFieldName(form.current?.fields || []);
|
|
429
|
+
const fields = form.current.version === 'v2' ? form.current?.fields || [] : addFieldName(form.current?.fields || []);
|
|
413
430
|
|
|
414
431
|
form.current = {
|
|
432
|
+
...form.current,
|
|
415
433
|
_id: _makeNewID(),
|
|
416
434
|
_rev: 1,
|
|
417
435
|
form: form_id,
|
|
@@ -1191,7 +1191,7 @@ UUFlowManager.getInstanceName = async function (instance, vals) {
|
|
|
1191
1191
|
if (form_v.fields) {
|
|
1192
1192
|
for (const field of form_v.fields) {
|
|
1193
1193
|
if (["select", "multiSelect", "radio"].includes(field.type)) {
|
|
1194
|
-
const fieldOptions = field.options?.split("\n").map((n) => {
|
|
1194
|
+
const fieldOptions = _.isArray(field.options) ? field.options : field.options?.split("\n").map((n) => {
|
|
1195
1195
|
const itemSplits = n.split(":");
|
|
1196
1196
|
return {
|
|
1197
1197
|
label: itemSplits[0],
|
|
@@ -35,6 +35,9 @@ amis_schema: |-
|
|
|
35
35
|
"rowClassNameExpr": "<%= data.__selected === true ? 'hidden' : '' %>",
|
|
36
36
|
"perPage": 20
|
|
37
37
|
},
|
|
38
|
+
"searchable_default": {
|
|
39
|
+
"submit_date": "${[STARTOF(DATEMODIFY(NOW(), -180, 'day'), 'day'),ENDOF(NOW(), 'day')]}"
|
|
40
|
+
},
|
|
38
41
|
"amis": {
|
|
39
42
|
"id": "u:f0273e374d19",
|
|
40
43
|
"embed": true,
|
|
@@ -47,7 +50,7 @@ amis_schema: |-
|
|
|
47
50
|
"source": {
|
|
48
51
|
"method": "post",
|
|
49
52
|
"url": "${context.rootUrl}/graphql",
|
|
50
|
-
"requestAdaptor": "var
|
|
53
|
+
"requestAdaptor": "var searchableDefaultConfig = {\"submit_date\": \"${[STARTOF(DATEMODIFY(NOW(), -180, 'day'), 'day'),ENDOF(NOW(), 'day')]}\"};\nvar filterFormValues = JSON.parse(JSON.stringify(searchableDefaultConfig));\nif (_.isObject(filterFormValues)) {\n _.each(filterFormValues, function(v, k) {\n var isAmisFormulaValue = typeof v === \"string\" && v.indexOf(\"${\") > -1;\n if (isAmisFormulaValue) {\n filterFormValues[k] = AmisCore.evaluate(v, context);\n }\n });\n var fields = api.data.$self.uiSchema && api.data.$self.uiSchema.fields;\n filterFormValues = SteedosUI.getSearchFilterFormValues(filterFormValues, fields);\n}\nvar selfData = JSON.parse(JSON.stringify(api.data.$self));\nvar searchableFilter = SteedosUI.getSearchFilter(Object.assign({}, filterFormValues, selfData)) || []; const { pageNo, pageSize, keywords = '' } = api.data;\nvar keywordsFilters = SteedosUI.getKeywordsSearchFilter(api.data.$self.__keywords_lookup__related_instances__to__instances, [\"name\", \"flow_name\",\"submitter\",\"submit_date\"]);\nif (keywordsFilters && keywordsFilters.length > 0) {\n searchableFilter.push(keywordsFilters);\n}\nconsole.log(\"===\", JSON.stringify(api.data));\napi.data = {\n query: `\n query{\n rows: instances__getRelatedInstances(keywords: \"${keywords}\", top: ${pageSize || 20}, skip: ${(pageNo - 1) * pageSize}, filters: ${JSON.stringify(searchableFilter)}){\n _id,\n name,\n flow_name,\n submit_date,\n submitter\n _display:_ui{\n submit_date,\n submitter\n }\n },\n count: instances__getRelatedInstances__count(filters: ${JSON.stringify(searchableFilter)}, keywords: \"${keywords}\")\n }\n `\n};\nreturn api;",
|
|
51
54
|
"headers": {
|
|
52
55
|
"Authorization": "Bearer ${context.tenantId},${context.authToken}"
|
|
53
56
|
},
|
|
@@ -64,7 +64,7 @@ amis_schema: |-
|
|
|
64
64
|
"componentId": "",
|
|
65
65
|
"args": {
|
|
66
66
|
"blank": false,
|
|
67
|
-
"url": "/app/${appId}
|
|
67
|
+
"url": "/app/${appId}/${side_listview_id === 'inbox' || side_listview_id === 'outbox' ? 'instance_tasks' : 'instances'}/grid/${side_listview_id}"
|
|
68
68
|
},
|
|
69
69
|
"actionType": "url"
|
|
70
70
|
}
|
|
@@ -99,6 +99,26 @@ router.get('/am/designer/startup/v2', async function (req, res) {
|
|
|
99
99
|
|
|
100
100
|
let form = await formCollection.findOne({ _id: flow.form }, { projection: { historys: 0 } });
|
|
101
101
|
|
|
102
|
+
// Resolve created_by / modified_by user names for display
|
|
103
|
+
let usersCollection = await getCollection('users');
|
|
104
|
+
const userIds = [flow.created_by, flow.modified_by].filter(Boolean);
|
|
105
|
+
if (userIds.length > 0) {
|
|
106
|
+
const userDocs = await usersCollection.find(
|
|
107
|
+
{ _id: { $in: userIds } },
|
|
108
|
+
{ projection: { name: 1 } }
|
|
109
|
+
).toArray();
|
|
110
|
+
const userMap = {};
|
|
111
|
+
for (const u of userDocs) {
|
|
112
|
+
userMap[u._id] = u.name;
|
|
113
|
+
}
|
|
114
|
+
if (flow.created_by && userMap[flow.created_by]) {
|
|
115
|
+
flow.created_by_name = userMap[flow.created_by];
|
|
116
|
+
}
|
|
117
|
+
if (flow.modified_by && userMap[flow.modified_by]) {
|
|
118
|
+
flow.modified_by_name = userMap[flow.modified_by];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
102
122
|
res.send({
|
|
103
123
|
flow,
|
|
104
124
|
form,
|
|
@@ -464,6 +484,7 @@ router.put('/am/flows', async function (req, res) {
|
|
|
464
484
|
let formId = flowCome["form"];
|
|
465
485
|
let flowId = flowCome["id"];
|
|
466
486
|
let upgraded = flowCome['upgraded'];
|
|
487
|
+
delete flowCome['decription'];
|
|
467
488
|
let now = new Date();
|
|
468
489
|
|
|
469
490
|
await designerManager.checkBeforeFlow(spaceId, formId);
|
|
@@ -565,7 +586,7 @@ router.put('/am/flows', async function (req, res) {
|
|
|
565
586
|
updateObj.$set.is_valid = flowCome['is_valid'];
|
|
566
587
|
updateObj.$set.flowtype = flowCome['flowtype'];
|
|
567
588
|
updateObj.$set.help_text = flowCome['help_text'];
|
|
568
|
-
updateObj.$set.
|
|
589
|
+
updateObj.$set.description = flowCome['descriptions'];
|
|
569
590
|
updateObj.$set.error_message = flowCome['error_message'];
|
|
570
591
|
updateObj.$set.modified = now;
|
|
571
592
|
updateObj.$set.modified_by = userId;
|
|
@@ -584,6 +605,20 @@ router.put('/am/flows', async function (req, res) {
|
|
|
584
605
|
updateObj.$set.perms = flowCome['perms'];
|
|
585
606
|
}
|
|
586
607
|
|
|
608
|
+
// Save flow-level properties
|
|
609
|
+
if (flowCome['allow_select_step'] !== undefined) {
|
|
610
|
+
updateObj.$set.allow_select_step = flowCome['allow_select_step'];
|
|
611
|
+
}
|
|
612
|
+
if (flowCome['timeout_auto_submit'] !== undefined) {
|
|
613
|
+
updateObj.$set.timeout_auto_submit = flowCome['timeout_auto_submit'];
|
|
614
|
+
}
|
|
615
|
+
if (flowCome['auto_remind'] !== undefined) {
|
|
616
|
+
updateObj.$set.auto_remind = flowCome['auto_remind'];
|
|
617
|
+
}
|
|
618
|
+
if (flowCome['enable_distribute_instance_related'] !== undefined) {
|
|
619
|
+
updateObj.$set.enable_distribute_instance_related = flowCome['enable_distribute_instance_related'];
|
|
620
|
+
}
|
|
621
|
+
|
|
587
622
|
// flow对象上添加categoryId
|
|
588
623
|
let form = await formCollection.findOne({_id: flow.form}, {
|
|
589
624
|
projection: {
|
|
@@ -35,7 +35,7 @@ router.get('/api/amisFormDesign', requireAuthentication, async function (req, re
|
|
|
35
35
|
|
|
36
36
|
const retUrl = process.env.ROOT_URL + `/app/admin/flows/view/${flowId}`
|
|
37
37
|
const steedosBuilderUrl = process.env.STEEDOS_BUILDER_URL || 'https://builder.steedos.cn';
|
|
38
|
-
const builderHost = `${steedosBuilderUrl}/amis?${assetUrl}&retUrl=${retUrl}`;
|
|
38
|
+
const builderHost = `${steedosBuilderUrl}/amis?${assetUrl}&retUrl=${retUrl}&unpkgUrl=${process.env.STEEDOS_UNPKG_URL || 'https://unpkg.steedos.cn'}`;
|
|
39
39
|
|
|
40
40
|
// let data = fs.readFileSync(__dirname+'/design.html', 'utf8');
|
|
41
41
|
// res.send(data.replace('SteedosBuilderHost',steedosBuilderHost).replace('DataContext', JSON.stringify(dataContext)));
|
|
@@ -77,17 +77,18 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
|
|
|
77
77
|
- "reference" — 对象选择(选择指定对象的记录,支持多选,可展示记录的多个字段)
|
|
78
78
|
- "richtext" — 富文本(支持格式化文本、颜色、链接等)
|
|
79
79
|
- required: 布尔值,是否必填
|
|
80
|
-
- colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
|
|
81
80
|
- description: 字符串,字段说明/提示文字,支持 HTML
|
|
82
81
|
- placeholder: 字符串,输入占位提示
|
|
83
82
|
- defaultValue: 字符串,默认值
|
|
83
|
+
- readonly: 布尔值,是否只读(只读字段不可编辑,仅展示)
|
|
84
|
+
- showLabel: 布尔值,是否显示字段标签,默认 true。设为 false 时隐藏标签,只显示输入区域
|
|
84
85
|
|
|
85
86
|
### 特定类型的额外字段
|
|
86
87
|
|
|
87
88
|
- select/multiSelect/radio 类型:
|
|
88
89
|
- options: 数组,选项列表,格式 [{ "label": "显示文本", "value": "值" }]
|
|
89
90
|
|
|
90
|
-
- number
|
|
91
|
+
- number 类型(纯手工输入的数字字段,**不含公式**;如果字段有公式/自动计算,必须用 formula 类型):
|
|
91
92
|
- min: 数字,最小值
|
|
92
93
|
- max: 数字,最大值
|
|
93
94
|
- precision: 数字,小数位数
|
|
@@ -97,8 +98,10 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
|
|
|
97
98
|
- maxLength: 数字,最大字符数
|
|
98
99
|
- minLength: 数字,最小字符数
|
|
99
100
|
|
|
100
|
-
- formula
|
|
101
|
+
- formula 类型(凡是有公式/自动计算的字段,都必须用此类型,禁止用 number+formula 组合):
|
|
101
102
|
- formula: 字符串,公式表达式。语法:用 \${} 包裹表达式,内部直接使用字段名(不需要额外括号)。没有 \${} 的部分作为纯文本。
|
|
103
|
+
- valueType: 字符串,公式计算结果的值类型。可选值:"number"(数值,默认)、"string"(字符串,如 CONCAT/RMB/TEXT 等返回文本的公式)、"date"(日期)、"dateTime"(日期时间)、"boolean"(布尔)。根据公式计算结果的实际数据类型设置。示例:算术运算/SUM/ROUND 等→ "number";CONCAT/RMB/TEXT 等→ "string";DATE/TODAY 等→ "date"
|
|
104
|
+
- precision: 数字,小数位数(仅当 valueType 为 "number" 时有效,如金额合计字段设 precision: 2)
|
|
102
105
|
- ⚠️ 严格限制:只能使用下方列出的函数,禁止使用任何 Excel 函数、JavaScript 函数或其他未列出的函数(如 NETWORKDAYS、WORKDAY、EDATE、SUMIF、VLOOKUP、parseFloat、Math.floor 等一律不可用)。如果某个功能无法用已有函数实现,用 IF/AND/OR 的组合或算术运算代替,不要臆造函数名。
|
|
103
106
|
- 支持的函数(完整列表,共 36 个,仅此列表内的函数可用):
|
|
104
107
|
- 聚合:SUM(...) 求和,AVG/AVERAGE(...) 平均,MAX(...) 最大,MIN(...) 最小,COUNT(...) 计数(空/0不计)
|
|
@@ -130,6 +133,9 @@ router.post('/am/ai/form-design', async function auth(req, res, next) {
|
|
|
130
133
|
- table 类型(子表):
|
|
131
134
|
- children: 数组,子字段列表,每个子字段结构与普通字段相同(但不能嵌套 table/section/grid)
|
|
132
135
|
- 子字段可用类型:text, textarea, number, date, datetime, time, select, multiSelect, checkbox, radio, file, image, member, memberMulti, org, orgMulti, formula
|
|
136
|
+
- showRowNumber: 布尔值,是否显示行号,默认 true
|
|
137
|
+
- minRows: 数字,最少行数(用户不能删减到少于此数)
|
|
138
|
+
- maxRows: 数字,最多行数(用户不能新增超过此数)
|
|
133
139
|
|
|
134
140
|
- grid 类型(网格):
|
|
135
141
|
- 用于复杂的自由表格布局,支持任意行列数、合并/拆分单元格、嵌入字段或静态文本
|
|
@@ -235,12 +241,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
235
241
|
## 规则
|
|
236
242
|
1. 修改时尽可能保留用户已有字段的 name,只改需要改的部分
|
|
237
243
|
2. 新增字段的 name 必须是合法的 JavaScript 变量名(字母/下划线开头,仅含字母、数字、下划线),要语义清晰
|
|
238
|
-
3. section 分组、table 子表、grid
|
|
244
|
+
3. section 分组、table 子表、grid 网格始终独占整行,不需要设置 colspan
|
|
239
245
|
4. formula 字段的公式表达式使用 \${} 语法,内部直接引用字段名,如 "\${unit_price * quantity}";公式内部不能再嵌套 \${},错误示例:"\${RMB(\${amount})}",正确示例:"\${RMB(amount)}"
|
|
240
246
|
5. formula 字段只能使用上方"支持的函数(完整列表)"中列出的 36 个函数,禁止使用任何 Excel 专有函数(NETWORKDAYS、WORKDAY、EDATE、SUMIF、VLOOKUP、IFERROR、EOMONTH 等)或 JavaScript 方法(Math.floor、parseFloat、toString 等),违反此规则会导致运行时报错
|
|
241
247
|
6. 如果用户只要求修改脚本/事件,则 fields 保持原样不变
|
|
242
248
|
7. 如果用户只要求修改字段,则 events 保持原样不变
|
|
243
249
|
8. 返回完整的 fields 数组和 events 对象
|
|
250
|
+
9. **grid 类型字段必须同时返回 gridRows、gridCols、gridData、children 四个属性**,缺少任何一个都会导致网格渲染为空白。gridData 中 cellType 为 "input" 的单元格必须有 fieldId,且该 fieldId 需在 children 数组中有对应的子字段
|
|
251
|
+
10. 修改已有 grid 字段时,必须保留其完整的 gridData 和 children 数据,不要丢弃未被修改的子字段。如果用户未要求改动 grid 内容,应原样返回 grid 的所有属性
|
|
252
|
+
11. 每个字段的类型特有属性必须完整返回:number 类型的 min/max/precision/thousandSeparator、text/textarea 的 maxLength/minLength、lookup 的 reference_to/lookupLabelField/lookupFillRules、member/org 的 pickerFillRules、reference 的 reference_to/displayFields 等。不要遗漏这些属性
|
|
253
|
+
12. ⚠️ **type 与 formula 属性的严格对应**:只要字段包含公式表达式(formula 属性),其 type 就**必须**是 "formula",绝对不能是 "number"、"text" 或其他类型。"number" 类型仅用于用户手工输入数字的字段(无公式)。错误示例:{ type: "number", formula: "${a + b}" },正确示例:{ type: "formula", formula: "${a + b}", precision: 2 }。grid/table 的 children 子字段同样适用此规则
|
|
244
254
|
|
|
245
255
|
## 额外待转换的数据(升级场景)
|
|
246
256
|
|
|
@@ -266,7 +276,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
266
276
|
- 分析模板时,首先统计 HTML 表格展开所有 colspan 后的**实际最大总列数**(包含标签列和数据列),然后选择布局方案:
|
|
267
277
|
- **⭐ 优先使用方案二(grid 网格字段)**。只有当模板表格极其简单(总列数 ≤ 6 且无 rowspan/colspan)时才考虑方案一。如果你不确定该用哪个,请使用方案二。
|
|
268
278
|
|
|
269
|
-
#### 方案一:简单布局(tableColumns
|
|
279
|
+
#### 方案一:简单布局(tableColumns)
|
|
270
280
|
适用条件:模板 HTML 表格展开后总列数 ≤ 6(含标签列),每行最多 3 个输入字段,且无 rowspan/colspan 合并单元格。**如果模板中出现任何 rowspan 或 colspan,则不得使用方案一,必须使用方案二。**
|
|
271
281
|
|
|
272
282
|
1. **确定 tableColumns**(每行字段数):
|
|
@@ -274,19 +284,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
274
284
|
- 模板中每行有 2 个输入字段 → tableColumns=2
|
|
275
285
|
- 模板中每行有 3 个输入字段 → tableColumns=3(最大值)
|
|
276
286
|
|
|
277
|
-
2.
|
|
278
|
-
- tableColumns=2 时:每个字段默认 tableColspan=1,独占一行的字段 tableColspan=2
|
|
279
|
-
- tableColumns=3 时:每个字段默认 tableColspan=1,跨两列的 tableColspan=2,独占一行的 tableColspan=3
|
|
280
|
-
- 注意:tableColspan 的最大值等于 tableColumns
|
|
287
|
+
2. **字段排布**:每个字段默认占 1 列,按顺序自动流入行中
|
|
281
288
|
|
|
282
289
|
3. **分组布局**:模板中的分类标题应转为 type="section" 字段
|
|
283
290
|
|
|
284
291
|
4. **字段 label**:优先使用模板中该字段旁边的显示文本
|
|
285
292
|
|
|
286
293
|
#### 方案二:复杂表格布局(grid 网格字段)⭐ 默认首选方案
|
|
287
|
-
适用条件:模板 HTML 表格展开后总列数 > 6,或者存在**任何 rowspan 或 colspan**,或者有多级表头(主标题下有子标题),或者表格结构无法用 tableColumns(1-3)
|
|
294
|
+
适用条件:模板 HTML 表格展开后总列数 > 6,或者存在**任何 rowspan 或 colspan**,或者有多级表头(主标题下有子标题),或者表格结构无法用 tableColumns(1-3) 简单表达。**绝大多数审批单模板都应使用此方案。**
|
|
288
295
|
|
|
289
|
-
**必须使用 grid 网格字段来精确还原表格布局。禁止将复杂模板用 tableColumns
|
|
296
|
+
**必须使用 grid 网格字段来精确还原表格布局。禁止将复杂模板用 tableColumns 简化处理。如果模板有任何 rowspan/colspan 合并单元格,就必须使用 grid。**
|
|
290
297
|
|
|
291
298
|
转换步骤:
|
|
292
299
|
1. **分析 HTML 表格结构**:统计表格的总行数和总列数(考虑 colspan 展开后的最大列数),确定 gridRows 和 gridCols
|
|
@@ -296,7 +303,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
296
303
|
- HTML 的 \`rowspan\`/\`colspan\` 属性直接映射到 gridData 单元格的 \`rowspan\`/\`colspan\`
|
|
297
304
|
3. **构建 children 数组**:为每个 input 类型的单元格创建对应的子字段,通过 fieldId 关联
|
|
298
305
|
4. **设置 gridColumnWidths**:根据 HTML 表格各列的实际宽度比例设置百分比数组
|
|
299
|
-
5. **grid
|
|
306
|
+
5. **grid 字段始终独占整行**,tableColumns 可设为 1
|
|
300
307
|
|
|
301
308
|
转换示例 — 假设模板有一个 5 列表格(标题+4个数据列):
|
|
302
309
|
HTML: \`<tr><td rowspan="3">工程名称</td><td>材料费</td><td>{{values.clf_bq}}</td><td>{{values.clf_lj}}</td><td>{{values.clf_bz}}</td></tr>\`
|
|
@@ -304,7 +311,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
304
311
|
\`\`\`json
|
|
305
312
|
{
|
|
306
313
|
"type": "grid", "label": "费用明细", "name": "cost_grid",
|
|
307
|
-
"gridRows": 10, "gridCols": 5,
|
|
314
|
+
"gridRows": 10, "gridCols": 5,
|
|
308
315
|
"gridColumnWidths": [15, 15, 25, 25, 20],
|
|
309
316
|
"gridData": [
|
|
310
317
|
{ "row": 0, "col": 0, "cellType": "label", "value": "工程名称", "rowspan": 3, "align": "center" },
|
|
@@ -541,8 +548,6 @@ ${userRequest}
|
|
|
541
548
|
return s || 'field_' + Math.random().toString(36).slice(2, 6);
|
|
542
549
|
}
|
|
543
550
|
|
|
544
|
-
const tblCols = (typeof result.tableColumns === 'number' && result.tableColumns >= 1) ? Math.min(result.tableColumns, 3) : 2;
|
|
545
|
-
|
|
546
551
|
if (result.fields) {
|
|
547
552
|
for (const field of result.fields) {
|
|
548
553
|
// Backward compat: map legacy userPicker/groupPicker to new types
|
|
@@ -559,15 +564,9 @@ ${userRequest}
|
|
|
559
564
|
if (field.name && !jsIdentifierRe.test(field.name)) {
|
|
560
565
|
field.name = sanitizeName(field.name);
|
|
561
566
|
}
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
field.colspan = 12;
|
|
566
|
-
} else {
|
|
567
|
-
const aiColspan = Math.max(1, Math.min(Number(field.colspan) || 1, tblCols));
|
|
568
|
-
field.tableColspan = aiColspan;
|
|
569
|
-
field.colspan = (aiColspan >= tblCols) ? 12 : Math.round(12 / tblCols * aiColspan);
|
|
570
|
-
}
|
|
567
|
+
// Remove colspan/tableColspan from AI output — layout is determined by field type defaults
|
|
568
|
+
delete field.colspan;
|
|
569
|
+
delete field.tableColspan;
|
|
571
570
|
// Validate children types for table
|
|
572
571
|
if (field.type === 'table' && Array.isArray(field.children)) {
|
|
573
572
|
for (const child of field.children) {
|
|
@@ -590,6 +589,26 @@ ${userRequest}
|
|
|
590
589
|
}
|
|
591
590
|
}
|
|
592
591
|
}
|
|
592
|
+
// Validate children types for grid
|
|
593
|
+
if (field.type === 'grid' && Array.isArray(field.children)) {
|
|
594
|
+
for (const child of field.children) {
|
|
595
|
+
// Backward compat: map legacy types in children
|
|
596
|
+
if (child.type === 'userPicker') child.type = child.pickerMultiple ? 'memberMulti' : 'member';
|
|
597
|
+
if (child.type === 'groupPicker') child.type = child.pickerMultiple ? 'orgMulti' : 'org';
|
|
598
|
+
if (child.type === 'select' && child.pickerMultiple) child.type = 'multiSelect';
|
|
599
|
+
if (child.type === 'html') child.type = 'richtext';
|
|
600
|
+
if (child.type && !validTypes.includes(child.type)) {
|
|
601
|
+
child.type = 'text';
|
|
602
|
+
}
|
|
603
|
+
if (child.name && !jsIdentifierRe.test(child.name)) {
|
|
604
|
+
child.name = sanitizeName(child.name);
|
|
605
|
+
}
|
|
606
|
+
// Grid children cannot be section or grid
|
|
607
|
+
if (child.type === 'section' || child.type === 'grid') {
|
|
608
|
+
child.type = 'text';
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
593
612
|
}
|
|
594
613
|
}
|
|
595
614
|
|
|
@@ -315,8 +315,6 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
|
|
|
315
315
|
return s || 'field_' + Math.random().toString(36).slice(2, 6);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
const tblCols = (typeof result.tableColumns === 'number' && result.tableColumns >= 1) ? Math.min(result.tableColumns, 3) : 2;
|
|
319
|
-
|
|
320
318
|
if (result.fields) {
|
|
321
319
|
for (const field of result.fields) {
|
|
322
320
|
// Backward compat: map legacy userPicker/groupPicker to new types
|
|
@@ -332,15 +330,9 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
|
|
|
332
330
|
if (field.name && !jsIdentifierRe.test(field.name)) {
|
|
333
331
|
field.name = sanitizeName(field.name);
|
|
334
332
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
} else {
|
|
339
|
-
// AI outputs colspan as 1..tableColumns; convert to tableColspan + 12-grid colspan
|
|
340
|
-
const aiColspan = Math.max(1, Math.min(Number(field.colspan) || 1, tblCols));
|
|
341
|
-
field.tableColspan = aiColspan;
|
|
342
|
-
field.colspan = (aiColspan >= tblCols) ? 12 : Math.round(12 / tblCols * aiColspan);
|
|
343
|
-
}
|
|
333
|
+
// Remove colspan/tableColspan from AI output — layout is determined by field type defaults
|
|
334
|
+
delete field.colspan;
|
|
335
|
+
delete field.tableColspan;
|
|
344
336
|
if (field.type === 'table' && Array.isArray(field.children)) {
|
|
345
337
|
for (const child of field.children) {
|
|
346
338
|
// Backward compat: map legacy types in children
|
|
@@ -443,7 +435,6 @@ function buildSystemPrompt() {
|
|
|
443
435
|
- "richtext" — 富文本(支持格式化文本、颜色、链接等)
|
|
444
436
|
- "autoNumber" — 自动编号(只读字段,自动生成带前缀、日期、序号的唯一编号)
|
|
445
437
|
- required: 布尔值,是否必填
|
|
446
|
-
- colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
|
|
447
438
|
- description: 字符串,字段说明/提示文字,支持 HTML
|
|
448
439
|
- placeholder: 字符串,输入占位提示
|
|
449
440
|
- defaultValue: 字符串,默认值
|
|
@@ -453,7 +444,7 @@ function buildSystemPrompt() {
|
|
|
453
444
|
- select/multiSelect/radio 类型:
|
|
454
445
|
- options: 数组,选项列表,格式 [{ "label": "显示文本", "value": "值" }]
|
|
455
446
|
|
|
456
|
-
- number
|
|
447
|
+
- number 类型(纯手工输入的数字字段,**不含公式**;如果字段有公式/自动计算,必须用 formula 类型):
|
|
457
448
|
- min: 数字,最小值
|
|
458
449
|
- max: 数字,最大值
|
|
459
450
|
- precision: 数字,小数位数
|
|
@@ -463,8 +454,10 @@ function buildSystemPrompt() {
|
|
|
463
454
|
- maxLength: 数字,最大字符数
|
|
464
455
|
- minLength: 数字,最小字符数
|
|
465
456
|
|
|
466
|
-
- formula
|
|
457
|
+
- formula 类型(凡是有公式/自动计算的字段,都必须用此类型,禁止用 number+formula 组合):
|
|
467
458
|
- formula: 字符串,公式表达式。语法:用 \${} 包裹表达式,内部直接使用字段名(不需要额外括号)。没有 \${} 的部分作为纯文本。
|
|
459
|
+
- valueType: 字符串,公式计算结果的值类型。可选值:"number"(数值,默认)、"string"(字符串,如 CONCAT/RMB/TEXT 等返回文本的公式)、"date"(日期)、"dateTime"(日期时间)、"boolean"(布尔)。根据公式计算结果的实际数据类型设置。示例:算术运算/SUM/ROUND 等→ "number";CONCAT/RMB/TEXT 等→ "string";DATE/TODAY 等→ "date"
|
|
460
|
+
- precision: 数字,小数位数(仅当 valueType 为 "number" 时有效,如金额合计字段设 precision: 2)
|
|
468
461
|
- ⚠️ 严格限制:只能使用下方列出的函数,禁止使用任何 Excel 函数、JavaScript 函数或其他未列出的函数(如 NETWORKDAYS、WORKDAY、EDATE、SUMIF、VLOOKUP、parseFloat、Math.floor 等一律不可用)。如果某个功能无法用已有函数实现,用 IF/AND/OR 的组合或算术运算代替,不要臆造函数名。
|
|
469
462
|
- 支持的函数(完整列表,共 36 个,仅此列表内的函数可用):
|
|
470
463
|
- 聚合:SUM(...) 求和,AVG/AVERAGE(...) 平均,MAX(...) 最大,MIN(...) 最小,COUNT(...) 计数(空/0不计)
|
|
@@ -611,12 +604,13 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
611
604
|
## 规则
|
|
612
605
|
1. 修改时尽可能保留用户已有字段的 name,只改需要改的部分
|
|
613
606
|
2. 新增字段的 name 必须是合法的 JavaScript 变量名(字母/下划线开头,仅含字母、数字、下划线),要语义清晰
|
|
614
|
-
3. section 分组、table 子表、grid
|
|
607
|
+
3. section 分组、table 子表、grid 网格始终独占整行,不需要设置 colspan
|
|
615
608
|
4. formula 字段的公式表达式使用 \${} 语法,内部直接引用字段名,如 "\${unit_price * quantity}";公式内部不能再嵌套 \${},错误示例:"\${RMB(\${amount})}",正确示例:"\${RMB(amount)}"
|
|
616
609
|
5. formula 字段只能使用上方"支持的函数(完整列表)"中列出的 36 个函数,禁止使用任何 Excel 专有函数(NETWORKDAYS、WORKDAY、EDATE、SUMIF、VLOOKUP、IFERROR、EOMONTH 等)或 JavaScript 方法(Math.floor、parseFloat、toString 等),违反此规则会导致运行时报错
|
|
617
610
|
6. 如果用户只要求修改脚本/事件,则 fields 保持原样不变
|
|
618
611
|
7. 如果用户只要求修改字段,则 events 保持原样不变
|
|
619
612
|
8. 返回完整的 fields 数组和 events 对象
|
|
613
|
+
9. ⚠️ **type 与 formula 属性的严格对应**:只要字段包含公式表达式(formula 属性),其 type 就**必须**是 "formula",绝对不能是 "number"、"text" 或其他类型。"number" 类型仅用于用户手工输入数字的字段(无公式)。错误示例:{ type: "number", formula: "\${a + b}" },正确示例:{ type: "formula", formula: "\${a + b}", valueType: "number", precision: 2 }。grid/table 的 children 子字段同样适用此规则
|
|
620
614
|
|
|
621
615
|
## 额外待转换的数据(升级场景)
|
|
622
616
|
|
|
@@ -642,7 +636,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
642
636
|
- 分析模板时,首先统计 HTML 表格展开所有 colspan 后的**实际最大总列数**(包含标签列和数据列),然后选择布局方案:
|
|
643
637
|
- **⭐ 优先使用方案二(grid 网格字段)**。只有当模板表格极其简单(总列数 ≤ 6 且无 rowspan/colspan)时才考虑方案一。如果你不确定该用哪个,请使用方案二。
|
|
644
638
|
|
|
645
|
-
#### 方案一:简单布局(tableColumns
|
|
639
|
+
#### 方案一:简单布局(tableColumns)
|
|
646
640
|
适用条件:模板 HTML 表格展开后总列数 ≤ 6(含标签列),每行最多 3 个输入字段,且无 rowspan/colspan 合并单元格。**如果模板中出现任何 rowspan 或 colspan,则不得使用方案一,必须使用方案二。**
|
|
647
641
|
|
|
648
642
|
1. **确定 tableColumns**(每行字段数):
|
|
@@ -650,19 +644,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
650
644
|
- 模板中每行有 2 个输入字段 → tableColumns=2
|
|
651
645
|
- 模板中每行有 3 个输入字段 → tableColumns=3(最大值)
|
|
652
646
|
|
|
653
|
-
2.
|
|
654
|
-
- tableColumns=2 时:每个字段默认 tableColspan=1,独占一行的字段 tableColspan=2
|
|
655
|
-
- tableColumns=3 时:每个字段默认 tableColspan=1,跨两列的 tableColspan=2,独占一行的 tableColspan=3
|
|
656
|
-
- 注意:tableColspan 的最大值等于 tableColumns
|
|
647
|
+
2. **字段排布**:每个字段默认占 1 列,按顺序自动流入行中
|
|
657
648
|
|
|
658
649
|
3. **分组布局**:模板中的分类标题应转为 type="section" 字段
|
|
659
650
|
|
|
660
651
|
4. **字段 label**:优先使用模板中该字段旁边的显示文本
|
|
661
652
|
|
|
662
653
|
#### 方案二:复杂表格布局(grid 网格字段)⭐ 默认首选方案
|
|
663
|
-
适用条件:模板 HTML 表格展开后总列数 > 6,或者存在**任何 rowspan 或 colspan**,或者有多级表头(主标题下有子标题),或者表格结构无法用 tableColumns(1-3)
|
|
654
|
+
适用条件:模板 HTML 表格展开后总列数 > 6,或者存在**任何 rowspan 或 colspan**,或者有多级表头(主标题下有子标题),或者表格结构无法用 tableColumns(1-3) 简单表达。**绝大多数审批单模板都应使用此方案。**
|
|
664
655
|
|
|
665
|
-
**必须使用 grid 网格字段来精确还原表格布局。禁止将复杂模板用 tableColumns
|
|
656
|
+
**必须使用 grid 网格字段来精确还原表格布局。禁止将复杂模板用 tableColumns 简化处理。如果模板有任何 rowspan/colspan 合并单元格,就必须使用 grid。**
|
|
666
657
|
|
|
667
658
|
转换步骤:
|
|
668
659
|
1. **分析 HTML 表格结构**:统计表格的总行数和总列数(考虑 colspan 展开后的最大列数),确定 gridRows 和 gridCols
|
|
@@ -672,7 +663,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
672
663
|
- HTML 的 \`rowspan\`/\`colspan\` 属性直接映射到 gridData 单元格的 \`rowspan\`/\`colspan\`
|
|
673
664
|
3. **构建 children 数组**:为每个 input 类型的单元格创建对应的子字段,通过 fieldId 关联
|
|
674
665
|
4. **设置 gridColumnWidths**:根据 HTML 表格各列的实际宽度比例设置百分比数组
|
|
675
|
-
5. **grid
|
|
666
|
+
5. **grid 字段始终独占整行**,tableColumns 可设为 1
|
|
676
667
|
|
|
677
668
|
转换示例 — 假设模板有一个 5 列表格(标题+4个数据列):
|
|
678
669
|
HTML: \`<tr><td rowspan="3">工程名称</td><td>材料费</td><td>{{values.clf_bq}}</td><td>{{values.clf_lj}}</td><td>{{values.clf_bz}}</td></tr>\`
|
|
@@ -680,7 +671,7 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
680
671
|
\`\`\`json
|
|
681
672
|
{
|
|
682
673
|
"type": "grid", "label": "费用明细", "name": "cost_grid",
|
|
683
|
-
"gridRows": 10, "gridCols": 5,
|
|
674
|
+
"gridRows": 10, "gridCols": 5,
|
|
684
675
|
"gridColumnWidths": [15, 15, 25, 25, 20],
|
|
685
676
|
"gridData": [
|
|
686
677
|
{ "row": 0, "col": 0, "cellType": "label", "value": "工程名称", "rowspan": 3, "align": "center" },
|
|
@@ -205,7 +205,7 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
205
205
|
const key = f.code;
|
|
206
206
|
let old_v = old_values[key];
|
|
207
207
|
if (old_v) {
|
|
208
|
-
const fieldOptions = f.options?.split("\n").map(n => {
|
|
208
|
+
const fieldOptions = _.isArray(f.options) ? f.options : f.options?.split("\n").map(n => {
|
|
209
209
|
const itemSplits = n.split(":");
|
|
210
210
|
return {
|
|
211
211
|
label: itemSplits[0],
|
|
@@ -288,7 +288,7 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
288
288
|
const key = field.code;
|
|
289
289
|
let old_v = old_values[key];
|
|
290
290
|
if (old_v) {
|
|
291
|
-
const fieldOptions = field.options?.split("\n").map(n => {
|
|
291
|
+
const fieldOptions = _.isArray(field.options) ? field.options : field.options?.split("\n").map(n => {
|
|
292
292
|
const itemSplits = n.split(":");
|
|
293
293
|
return {
|
|
294
294
|
label: itemSplits[0],
|
|
@@ -29,7 +29,24 @@ const getFieldValue = (values, code)=>{
|
|
|
29
29
|
|
|
30
30
|
const getFormFieldValue = (fields, values, fieldId)=>{
|
|
31
31
|
const code = getFieldName(fields, fieldId);
|
|
32
|
-
|
|
32
|
+
const _values = getFieldValue(values, code);
|
|
33
|
+
|
|
34
|
+
if(_.isArray(_values)){
|
|
35
|
+
return _.map(_values, (value)=>{
|
|
36
|
+
if(_.isObject(value) && value.id){
|
|
37
|
+
return value.id;
|
|
38
|
+
}else{
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}else{
|
|
43
|
+
if(_.isObject(_values) && _values.id){
|
|
44
|
+
return _values.id;
|
|
45
|
+
}else{
|
|
46
|
+
return _values;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
/**
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
title: "<%=locale%>" === 'zh-CN' ? '流程表单设计器': 'Flow Form Designer',
|
|
71
71
|
saveText: "<%=locale%>" === 'zh-CN' ? '保存': 'Save',
|
|
72
72
|
deployText: "<%=locale%>" === 'zh-CN' ? '发布': 'Deploy',
|
|
73
|
+
unpkgUrl: "<%=unpkgUrl%>"
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
const getArgumentsList = (func)=>{
|
|
@@ -279,7 +280,7 @@
|
|
|
279
280
|
* 保持同步。
|
|
280
281
|
*/
|
|
281
282
|
const _getSafeCode = (code) => {
|
|
282
|
-
return code.replace(/(/g, '_').replace(/)/g, '').replace(/\(/g, '_').replace(/\)/g, '').replace(/、/g, '_').replace(/,/g, '_').replace(/%/g, '_').replace(/=/g, '_');
|
|
283
|
+
return code.replace(/(/g, '_').replace(/)/g, '').replace(/\(/g, '_').replace(/\)/g, '').replace(/、/g, '_').replace(/,/g, '_').replace(/%/g, '_').replace(/=/g, '_').replace(/:/g, '_').replace(/\//g, '_').replace(/-/g, '_');
|
|
283
284
|
};
|
|
284
285
|
|
|
285
286
|
/**
|
|
@@ -295,6 +296,14 @@
|
|
|
295
296
|
if (formula.trim() === '{now}') return '${NOW()}';
|
|
296
297
|
|
|
297
298
|
let newFormula = formula;
|
|
299
|
+
|
|
300
|
+
// 预处理:修正生产数据中的非标准聚合函数语法
|
|
301
|
+
// B: 全角括号 sum({x}) → sum({x})
|
|
302
|
+
newFormula = newFormula.replace(/(sum|average|count|max|min|numToRMB)\s*(/ig, '$1(');
|
|
303
|
+
newFormula = newFormula.replace(/\})/g, '})');
|
|
304
|
+
// C: 缺失括号 sum{x} → sum({x})
|
|
305
|
+
newFormula = newFormula.replace(/(sum|average|count|max|min|numToRMB)\{([^{}]*)\}/ig, '$1({$2})');
|
|
306
|
+
|
|
298
307
|
const isFunction = newFormula.match(/(sum|average|count|max|min|numToRMB)\s*\(/i);
|
|
299
308
|
const hasFieldRef = newFormula.match(/\{[^{}]+\}/);
|
|
300
309
|
const isOperator = newFormula.match(/[\+\-\*\/]/) && hasFieldRef && newFormula.indexOf("}.") < 0;
|
|
@@ -304,6 +313,9 @@
|
|
|
304
313
|
const _isContextVariable = (code) => code === 'applicant' || code === 'approver';
|
|
305
314
|
|
|
306
315
|
if (isFunction || isOperator || isObjectField || isDotField) {
|
|
316
|
+
// 将数学分组方括号 [] 转为圆括号 ()
|
|
317
|
+
newFormula = newFormula.replace(/\[/g, '(').replace(/\]/g, ')');
|
|
318
|
+
|
|
307
319
|
if (isFunction) {
|
|
308
320
|
newFormula = newFormula.replace(/sum\s*\(/ig, 'SUM(');
|
|
309
321
|
newFormula = newFormula.replace(/average\s*\(/ig, 'AVG(');
|