cloud-web-corejs 1.1.0-dev.19 → 1.1.0-dev.21

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.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/src/components/baseInputExport/mixins.js +0 -6
  3. package/src/components/xform/docs/2026-09 /345/212/250/346/200/201/345/255/227/346/256/265/350/247/204/345/210/231/344/270/216/346/265/201/347/250/213/346/216/247/345/210/266/346/211/247/350/241/214/351/241/272/345/272/217/350/220/275/345/234/260/346/226/271/346/241/210.md" +409 -0
  4. package/src/components/xform/form-designer/designer.js +2086 -2084
  5. package/src/components/xform/form-designer/form-widget/dialog/importDialogMixin.js +34 -0
  6. package/src/components/xform/form-designer/form-widget/field-widget/fieldMixin.js +13 -1
  7. package/src/components/xform/form-designer/setting-panel/form-dynamicField-setting.vue +687 -602
  8. package/src/components/xform/form-designer/setting-panel/form-setting.vue +74 -9
  9. package/src/components/xform/form-designer/setting-panel/property-editor/field-import-button/import2-button-editor.vue +8 -0
  10. package/src/components/xform/form-render/container-item/data-table-mixin.js +6304 -6285
  11. package/src/components/xform/form-render/container-item/dynamicFieldMixin.js +55 -1
  12. package/src/components/xform/form-render/container-item/tab-item.vue +138 -129
  13. package/src/components/xform/form-render/fieldControlEngine.js +218 -0
  14. package/src/components/xform/form-render/fieldControlMixin.js +534 -0
  15. package/src/components/xform/form-render/index.vue +164 -153
  16. package/src/components/xform/form-render/indexMixin.js +5415 -5331
  17. package/src/components/xform/mixins/defaultHandle.js +713 -707
  18. package/src/components/xform/utils/util.js +1710 -1708
  19. package/src/views/user/home/default.vue +6 -3
  20. package/src/views/user/home/wjl/content.vue +3 -2
  21. package/src/components/xform/form-render/container-item/dynamicFieldEngine.js +0 -285
  22. package/src/components/xform/form-render/formDynamicFieldMixin.js +0 -131
@@ -1,602 +1,687 @@
1
- <template>
2
- <div class="form-dynamic-field-setting">
3
- <el-form-item label="启用动态字段">
4
- <el-switch v-model="formConfig.dynamicFieldEnabled"></el-switch>
5
- <div class="tip-text">
6
- 根据表单数据/后台脚本动态控制整个表单内任意字段、容器的显隐等状态
7
- </div>
8
- </el-form-item>
9
-
10
- <template v-if="formConfig.dynamicFieldEnabled">
11
- <el-form-item label="数据来源" class="form-item-label-top">
12
- <el-radio-group v-model="formConfig.dynamicFieldSourceType" size="mini">
13
- <el-radio-button label="local">本地规则</el-radio-button>
14
- <el-radio-button label="script">后台脚本</el-radio-button>
15
- </el-radio-group>
16
- </el-form-item>
17
-
18
- <template v-if="formConfig.dynamicFieldSourceType === 'local'">
19
- <el-form-item label="字段规则" class="el-form-item-second">
20
- <el-button
21
- type="primary"
22
- plain
23
- size="mini"
24
- icon="el-icon-setting"
25
- @click="openRuleDialog"
26
- >
27
- 配置规则({{ (formConfig.dynamicFieldRules || []).length }})
28
- </el-button>
29
- </el-form-item>
30
- </template>
31
-
32
- <template v-if="formConfig.dynamicFieldSourceType === 'script'">
33
- <el-form-item class="form-item-label-top el-form-item-second" label="脚本编码">
34
- <el-input
35
- v-model="formConfig.dynamicFieldScriptCode"
36
- size="mini"
37
- placeholder="scriptCode 或 formCode/scriptCode"
38
- ></el-input>
39
- </el-form-item>
40
- <el-form-item label="额外参数" class="el-form-item-second">
41
- <a
42
- href="javascript:void(0);"
43
- class="a-link link-oneLind"
44
- @click="editDynamicFieldScriptParam"
45
- >
46
- <span>{{ formConfig.dynamicFieldScriptParam }}</span>
47
- <i class="el-icon-edit"></i>
48
- </a>
49
- <div class="tip-text">
50
- 格式: return { stage:
51
- formModel.status }
52
- </div>
53
- </el-form-item>
54
- <el-form-item class="form-item-label-top el-form-item-second" label="触发字段">
55
- <el-select
56
- v-model="formConfig.dynamicFieldTriggerFields"
57
- multiple
58
- filterable
59
- size="mini"
60
- style="width: 100%"
61
- placeholder="字段值变化时重新请求后台"
62
- >
63
- <el-option
64
- v-for="(n, idx) in treeOptions"
65
- :key="n.value + '#' + idx"
66
- :value="n.value"
67
- :label="optPlainLabel(n)"
68
- :disabled="n.isContainer"
69
- >
70
- <span
71
- :style="{ paddingLeft: n.depth * 14 + 'px' }"
72
- :class="{ 'opt-container': n.isContainer }"
73
- >
74
- <span v-if="n.isContainer" class="opt-tag"
75
- >[{{ containerTypeLabel(n.type) }}]</span
76
- >{{ n.baseLabel }}
77
- </span>
78
- </el-option>
79
- </el-select>
80
- <div class="tip-text">为空时仅在初始化时请求一次后台脚本</div>
81
- </el-form-item>
82
- <div class="tip-text">
83
- 后台返回 fieldStates 的 key
84
- 可为字段名,也可为「命名容器」名以控制其显隐(容器仅 visible 生效)
85
- </div>
86
- </template>
87
- </template>
88
-
89
- <el-dialog
90
- title="表单动态字段规则"
91
- :visible.sync="ruleDialogVisible"
92
- :append-to-body="true"
93
- width="760px"
94
- custom-class="dynamic-field-rule-dialog"
95
- >
96
- <div class="rule-list">
97
- <div
98
- v-for="(rule, rIdx) in editingRules"
99
- :key="rule.id"
100
- class="rule-card"
101
- >
102
- <div class="rule-card-header">
103
- <span class="rule-title">规则 {{ rIdx + 1 }}</span>
104
- <el-button
105
- type="text"
106
- icon="el-icon-delete"
107
- class="danger-text"
108
- @click="removeRule(rIdx)"
109
- >
110
- 删除规则
111
- </el-button>
112
- </div>
113
-
114
- <div class="rule-section">
115
- <div class="rule-section-title">
116
- <span>当满足</span>
117
- <el-radio-group v-model="rule.logic" size="mini">
118
- <el-radio-button label="and">全部条件</el-radio-button>
119
- <el-radio-button label="or">任一条件</el-radio-button>
120
- </el-radio-group>
121
- </div>
122
- <div
123
- v-for="(cond, cIdx) in rule.conditions"
124
- :key="cIdx"
125
- class="rule-row"
126
- >
127
- <el-select
128
- v-model="cond.field"
129
- filterable
130
- size="mini"
131
- placeholder="字段"
132
- class="cell-field"
133
- >
134
- <el-option
135
- v-for="(n, idx) in treeOptions"
136
- :key="n.value + '#' + idx"
137
- :value="n.value"
138
- :label="optPlainLabel(n)"
139
- :disabled="n.isContainer"
140
- >
141
- <span
142
- :style="{ paddingLeft: n.depth * 14 + 'px' }"
143
- :class="{ 'opt-container': n.isContainer }"
144
- >
145
- <span v-if="n.isContainer" class="opt-tag"
146
- >[{{ containerTypeLabel(n.type) }}]</span
147
- >{{ n.baseLabel }}
148
- </span>
149
- </el-option>
150
- </el-select>
151
- <el-select
152
- v-model="cond.operator"
153
- size="mini"
154
- placeholder="操作符"
155
- class="cell-op"
156
- >
157
- <el-option
158
- v-for="op in operatorOptions"
159
- :key="op.value"
160
- :label="op.label"
161
- :value="op.value"
162
- ></el-option>
163
- </el-select>
164
- <el-input
165
- v-model="cond.value"
166
- size="mini"
167
- placeholder="值"
168
- class="cell-value"
169
- :disabled="op_noValue(cond.operator)"
170
- ></el-input>
171
- <el-button
172
- type="text"
173
- icon="el-icon-remove-outline"
174
- class="danger-text"
175
- @click="removeCondition(rule, cIdx)"
176
- ></el-button>
177
- </div>
178
- <el-button
179
- type="text"
180
- icon="el-icon-plus"
181
- @click="addCondition(rule)"
182
- >添加条件</el-button
183
- >
184
- </div>
185
-
186
- <div class="rule-section">
187
- <div class="rule-section-title"><span>则设置字段/容器</span></div>
188
- <div
189
- v-for="(t, tIdx) in rule.targets"
190
- :key="tIdx"
191
- class="rule-row target-row"
192
- >
193
- <el-select
194
- v-model="t.field"
195
- filterable
196
- size="mini"
197
- placeholder="目标字段/容器"
198
- class="cell-field"
199
- >
200
- <el-option
201
- v-for="(n, idx) in treeOptions"
202
- :key="n.value + '#' + idx"
203
- :value="n.value"
204
- :label="optPlainLabel(n)"
205
- :disabled="n.isContainer && !n.hasName"
206
- >
207
- <span
208
- :style="{ paddingLeft: n.depth * 14 + 'px' }"
209
- :class="{ 'opt-container': n.isContainer }"
210
- >
211
- <span v-if="n.isContainer" class="opt-tag"
212
- >[{{ containerTypeLabel(n.type) }}]</span
213
- >{{ n.baseLabel }}
214
- </span>
215
- </el-option>
216
- </el-select>
217
- <span class="target-attr">
218
- <span class="attr-label">显示</span>
219
- <el-select
220
- v-model="t.visible"
221
- size="mini"
222
- class="cell-tri"
223
- clearable
224
- placeholder="不变"
225
- >
226
- <el-option label="显示" :value="true"></el-option>
227
- <el-option label="隐藏" :value="false"></el-option>
228
- </el-select>
229
- </span>
230
- <span class="target-attr">
231
- <span class="attr-label">必填</span>
232
- <el-select
233
- v-model="t.required"
234
- size="mini"
235
- class="cell-tri"
236
- clearable
237
- placeholder="不变"
238
- >
239
- <el-option label="必填" :value="true"></el-option>
240
- <el-option label="非必填" :value="false"></el-option>
241
- </el-select>
242
- </span>
243
- <span class="target-attr">
244
- <span class="attr-label">只读</span>
245
- <el-select
246
- v-model="t.readonly"
247
- size="mini"
248
- class="cell-tri"
249
- clearable
250
- placeholder="不变"
251
- >
252
- <el-option label="只读" :value="true"></el-option>
253
- <el-option label="可编辑" :value="false"></el-option>
254
- </el-select>
255
- </span>
256
- <span class="target-attr">
257
- <span class="attr-label">禁用</span>
258
- <el-select
259
- v-model="t.disabled"
260
- size="mini"
261
- class="cell-tri"
262
- clearable
263
- placeholder="不变"
264
- >
265
- <el-option label="禁用" :value="true"></el-option>
266
- <el-option label="启用" :value="false"></el-option>
267
- </el-select>
268
- </span>
269
- <el-button
270
- type="text"
271
- icon="el-icon-remove-outline"
272
- class="danger-text"
273
- @click="removeTarget(rule, tIdx)"
274
- ></el-button>
275
- </div>
276
- <el-button type="text" icon="el-icon-plus" @click="addTarget(rule)"
277
- >添加目标字段</el-button
278
- >
279
- <div class="tip-text">
280
- 目标为容器时仅「显示」生效,必填/只读/禁用将被忽略
281
- </div>
282
- </div>
283
- </div>
284
-
285
- <el-button
286
- type="primary"
287
- plain
288
- icon="el-icon-plus"
289
- size="mini"
290
- @click="addRule"
291
- style="width: 100%"
292
- >
293
- 添加规则
294
- </el-button>
295
- </div>
296
-
297
- <div slot="footer">
298
- <el-button size="mini" @click="ruleDialogVisible = false"
299
- >取消</el-button
300
- >
301
- <el-button size="mini" type="primary" @click="saveRules"
302
- >确定</el-button
303
- >
304
- </div>
305
- </el-dialog>
306
-
307
- <el-dialog
308
- title="额外参数"
309
- :visible.sync="scriptParamDialogVisible"
310
- v-if="scriptParamDialogVisible"
311
- :show-close="true"
312
- custom-class="dialog-style list-dialog"
313
- v-dialog-drag
314
- :close-on-click-modal="false"
315
- :close-on-press-escape="false"
316
- :destroy-on-close="true"
317
- :append-to-body="true"
318
- :modal-append-to-body="true"
319
- >
320
- <div class="cont">
321
- <code-editor
322
- :mode="'javascript'"
323
- :readonly="false"
324
- v-model="editingScriptParam"
325
- ref="scriptParamEditor"
326
- ></code-editor>
327
- </div>
328
- <div slot="footer" class="dialog-footer">
329
- <el-button
330
- @click="scriptParamDialogVisible = false"
331
- class="button-sty"
332
- icon="el-icon-close"
333
- >
334
- {{ i18nt("designer.hint.cancel") }}
335
- </el-button>
336
- <el-button
337
- type="primary"
338
- @click="saveDynamicFieldScriptParam"
339
- class="button-sty"
340
- icon="el-icon-check"
341
- >
342
- {{ i18nt("designer.hint.confirm") }}
343
- </el-button>
344
- </div>
345
- </el-dialog>
346
- </div>
347
- </template>
348
-
349
- <script>
350
- import i18n from "../../../../components/xform/utils/i18n";
351
- import { buildWidgetTreeOptions } from "../../../../components/xform/form-render/container-item/dynamicFieldEngine";
352
-
353
- const NO_VALUE_OPERATORS = ["empty", "notEmpty"];
354
-
355
- export default {
356
- name: "form-dynamicField-setting",
357
- mixins: [i18n],
358
- props: {
359
- designer: Object,
360
- formConfig: Object,
361
- },
362
- data() {
363
- return {
364
- ruleDialogVisible: false,
365
- scriptParamDialogVisible: false,
366
- editingScriptParam: "",
367
- editingRules: [],
368
- operatorOptions: [
369
- { label: "等于", value: "eq" },
370
- { label: "不等于", value: "ne" },
371
- { label: "大于", value: "gt" },
372
- { label: "小于", value: "lt" },
373
- { label: "大于等于", value: "ge" },
374
- { label: "小于等于", value: "le" },
375
- { label: "属于(逗号分隔)", value: "in" },
376
- { label: "不属于(逗号分隔)", value: "nin" },
377
- { label: "包含", value: "includes" },
378
- { label: "为空", value: "empty" },
379
- { label: "不为空", value: "notEmpty" },
380
- ],
381
- };
382
- },
383
- computed: {
384
- // 带层级的字段/容器候选(整个表单)
385
- treeOptions() {
386
- return buildWidgetTreeOptions(
387
- (this.designer && this.designer.widgetList) || []
388
- );
389
- },
390
- },
391
- methods: {
392
- op_noValue(operator) {
393
- return NO_VALUE_OPERATORS.indexOf(operator) > -1;
394
- },
395
- // 选中态/过滤用的纯文本标签
396
- optPlainLabel(n) {
397
- if (n.isContainer) {
398
- return "[" + this.containerTypeLabel(n.type) + "] " + n.baseLabel;
399
- }
400
- return n.baseLabel;
401
- },
402
- containerTypeLabel(type) {
403
- const map = {
404
- grid: "栅格",
405
- table: "表格布局",
406
- "h5-table": "H5表格",
407
- tab: "标签页",
408
- "tab-pane": "标签项",
409
- card: "卡片",
410
- "vf-box": "区块",
411
- panel: "面板",
412
- detail: "单据详情",
413
- "detail-plain": "单据详情(简洁)",
414
- "detail-pane": "详情面板",
415
- "detail-h5": "H5详情",
416
- "h5-card": "H5卡片",
417
- "h5-card-pane": "H5卡片面板",
418
- "sub-form": "子表单",
419
- "grid-sub-form": "网格子表单",
420
- };
421
- return map[type] || type;
422
- },
423
- genId() {
424
- return "fdr_" + Date.now() + "_" + Math.floor(Math.random() * 10000);
425
- },
426
- editDynamicFieldScriptParam() {
427
- this.editingScriptParam = this.formConfig.dynamicFieldScriptParam || "";
428
- this.scriptParamDialogVisible = true;
429
- },
430
- saveDynamicFieldScriptParam() {
431
- const codeHints = this.$refs.scriptParamEditor.getEditorAnnotations();
432
- let syntaxErrorFlag = false;
433
- if (codeHints && codeHints.length > 0) {
434
- codeHints.forEach((chItem) => {
435
- if (chItem.type === "error") {
436
- syntaxErrorFlag = true;
437
- }
438
- });
439
- if (syntaxErrorFlag) {
440
- this.$message.error(
441
- this.i18nt("designer.setting.syntaxCheckWarning")
442
- );
443
- return;
444
- }
445
- }
446
- this.formConfig.dynamicFieldScriptParam = this.editingScriptParam;
447
- this.scriptParamDialogVisible = false;
448
- },
449
- openRuleDialog() {
450
- this.editingRules = JSON.parse(
451
- JSON.stringify(this.formConfig.dynamicFieldRules || [])
452
- );
453
- this.editingRules.forEach((rule) => {
454
- if (!rule.id) rule.id = this.genId();
455
- if (!rule.logic) rule.logic = "and";
456
- if (!rule.conditions) rule.conditions = [];
457
- if (!rule.targets) rule.targets = [];
458
- });
459
- this.ruleDialogVisible = true;
460
- },
461
- addRule() {
462
- this.editingRules.push({
463
- id: this.genId(),
464
- logic: "and",
465
- conditions: [{ field: "", operator: "eq", value: "" }],
466
- targets: [
467
- {
468
- field: "",
469
- visible: undefined,
470
- required: undefined,
471
- readonly: undefined,
472
- disabled: undefined,
473
- },
474
- ],
475
- });
476
- },
477
- removeRule(idx) {
478
- this.editingRules.splice(idx, 1);
479
- },
480
- addCondition(rule) {
481
- rule.conditions.push({ field: "", operator: "eq", value: "" });
482
- },
483
- removeCondition(rule, idx) {
484
- rule.conditions.splice(idx, 1);
485
- },
486
- addTarget(rule) {
487
- rule.targets.push({
488
- field: "",
489
- visible: undefined,
490
- required: undefined,
491
- readonly: undefined,
492
- disabled: undefined,
493
- });
494
- },
495
- removeTarget(rule, idx) {
496
- rule.targets.splice(idx, 1);
497
- },
498
- saveRules() {
499
- this.formConfig.dynamicFieldRules = JSON.parse(
500
- JSON.stringify(this.editingRules)
501
- );
502
- this.ruleDialogVisible = false;
503
- },
504
- },
505
- };
506
- </script>
507
-
508
- <style lang="scss" scoped>
509
- .tip-text {
510
- font-size: 12px;
511
- color: #909399;
512
- line-height: 1.4;
513
- margin-top: 2px;
514
- }
515
-
516
- .rule-list {
517
- max-height: 56vh;
518
- overflow-y: auto;
519
- }
520
-
521
- .rule-card {
522
- border: 1px solid #ebeef5;
523
- border-radius: 4px;
524
- padding: 10px 12px;
525
- margin-bottom: 12px;
526
- background: #fafafa;
527
- }
528
-
529
- .rule-card-header {
530
- display: flex;
531
- justify-content: space-between;
532
- align-items: center;
533
- margin-bottom: 6px;
534
-
535
- .rule-title {
536
- font-weight: bold;
537
- font-size: 13px;
538
- }
539
- }
540
-
541
- .rule-section {
542
- margin-top: 6px;
543
- }
544
-
545
- .rule-section-title {
546
- display: flex;
547
- align-items: center;
548
- gap: 8px;
549
- font-size: 13px;
550
- color: #606266;
551
- margin-bottom: 6px;
552
- }
553
-
554
- .rule-row {
555
- display: flex;
556
- align-items: center;
557
- gap: 6px;
558
- margin-bottom: 6px;
559
- flex-wrap: wrap;
560
- }
561
-
562
- .cell-field {
563
- width: 180px;
564
- }
565
-
566
- .cell-op {
567
- width: 120px;
568
- }
569
-
570
- .cell-value {
571
- width: 130px;
572
- }
573
-
574
- .cell-tri {
575
- width: 92px;
576
- }
577
-
578
- .target-attr {
579
- display: flex;
580
- align-items: center;
581
- gap: 2px;
582
-
583
- .attr-label {
584
- font-size: 12px;
585
- color: #909399;
586
- }
587
- }
588
-
589
- .danger-text {
590
- color: #f56c6c;
591
- }
592
-
593
- .opt-container {
594
- color: #409eff;
595
- }
596
-
597
- .opt-tag {
598
- color: #909399;
599
- font-size: 12px;
600
- margin-right: 4px;
601
- }
602
- </style>
1
+ <template>
2
+ <div class="form-dynamic-field-setting">
3
+ <el-form-item label="启用动态字段">
4
+ <el-switch v-model="formConfig.dynamicFieldEnabled"></el-switch>
5
+ <el-tooltip content="根据表单数据动态控制字段、容器的显隐、必填、只读和禁用状态。" placement="left" popper-class="xform-setting-help">
6
+ <i class="el-icon-info setting-help-icon" tabindex="0" aria-label="动态字段说明"></i>
7
+ </el-tooltip>
8
+ </el-form-item>
9
+
10
+ <template v-if="formConfig.dynamicFieldEnabled">
11
+ <div v-if="fieldControlTreeState.error" class="tip-text tree-options-error">{{ fieldControlTreeState.error }}</div>
12
+ <el-form-item label="执行方式">
13
+ <el-select :value="formConfig.dynamicFieldRunMode || 'auto'" size="mini"
14
+ @input="$set(formConfig, 'dynamicFieldRunMode', $event)">
15
+ <el-option label="按场景默认" value="auto" />
16
+ <el-option label="仅初始化" value="initialOnly" />
17
+ <el-option label="持续生效" value="continuous" />
18
+ </el-select>
19
+ <el-tooltip content="默认:有流程仅初始化,无流程持续联动。仅初始化保留首次结果;持续模式每轮均由流程规则收尾。" placement="left" popper-class="xform-setting-help">
20
+ <i class="el-icon-info setting-help-icon" tabindex="0" aria-label="执行方式说明"></i>
21
+ </el-tooltip>
22
+ </el-form-item>
23
+ <el-form-item label="字段规则" class="el-form-item-second">
24
+ <el-button
25
+ type="primary"
26
+ plain
27
+ size="mini"
28
+ icon="el-icon-setting"
29
+ @click="openRuleDialog"
30
+ >
31
+ 配置规则({{ (formConfig.dynamicFieldRules || []).length }})
32
+ </el-button>
33
+ </el-form-item>
34
+ </template>
35
+
36
+ <el-dialog
37
+ title="表单动态字段规则"
38
+ :visible.sync="ruleDialogVisible"
39
+ :append-to-body="true"
40
+ width="880px"
41
+ top="6vh"
42
+ custom-class="dynamic-field-rule-dialog"
43
+ >
44
+ <div class="rule-dialog-summary">配置触发条件与字段状态,规则按排列顺序执行。</div>
45
+ <div class="rule-list">
46
+ <div v-if="!editingRules.length" class="rule-empty">暂无规则,点击下方「添加规则」开始配置</div>
47
+ <div
48
+ v-for="(rule, rIdx) in editingRules"
49
+ :key="rule.id"
50
+ class="rule-card"
51
+ >
52
+ <div class="rule-card-header">
53
+ <span class="rule-title">规则 {{ rIdx + 1 }}</span>
54
+ <el-button
55
+ type="text"
56
+ icon="el-icon-delete"
57
+ class="danger-text"
58
+ @click="removeRule(rIdx)"
59
+ >
60
+ 删除规则
61
+ </el-button>
62
+ </div>
63
+
64
+ <div class="rule-section">
65
+ <div class="rule-section-title">
66
+ <span>当满足</span>
67
+ <el-radio-group v-model="rule.logic" size="mini">
68
+ <el-radio-button label="and">全部条件</el-radio-button>
69
+ <el-radio-button label="or">任一条件</el-radio-button>
70
+ </el-radio-group>
71
+ </div>
72
+ <div
73
+ v-for="(cond, cIdx) in rule.conditions"
74
+ :key="cIdx"
75
+ class="rule-row condition-row"
76
+ >
77
+ <el-select
78
+ v-model="cond.field"
79
+ filterable
80
+ size="mini"
81
+ placeholder="选择条件字段"
82
+ class="cell-field"
83
+ >
84
+ <el-option
85
+ v-for="(n, idx) in treeOptions"
86
+ :key="n.value + '#' + idx"
87
+ :value="n.value"
88
+ :label="optPlainLabel(n)"
89
+ :disabled="n.isContainer"
90
+ >
91
+ <span
92
+ :style="{ paddingLeft: n.depth * 14 + 'px' }"
93
+ :class="{ 'opt-container': n.isContainer }"
94
+ >
95
+ <span v-if="n.isContainer" class="opt-tag"
96
+ >[{{ containerTypeLabel(n.type) }}]</span
97
+ >{{ n.baseLabel }}
98
+ </span>
99
+ </el-option>
100
+ </el-select>
101
+ <el-select
102
+ v-model="cond.operator"
103
+ size="mini"
104
+ placeholder="操作符"
105
+ class="cell-op"
106
+ >
107
+ <el-option
108
+ v-for="op in operatorOptions"
109
+ :key="op.value"
110
+ :label="op.label"
111
+ :value="op.value"
112
+ ></el-option>
113
+ </el-select>
114
+ <el-input
115
+ v-model="cond.value"
116
+ size="mini"
117
+ placeholder="输入比较值"
118
+ class="cell-value"
119
+ :disabled="op_noValue(cond.operator)"
120
+ ></el-input>
121
+ <el-button
122
+ type="text"
123
+ icon="el-icon-remove-outline"
124
+ class="danger-text row-remove"
125
+ aria-label="删除条件"
126
+ @click="removeCondition(rule, cIdx)"
127
+ ></el-button>
128
+ </div>
129
+ <el-button
130
+ type="text"
131
+ icon="el-icon-plus"
132
+ @click="addCondition(rule)"
133
+ >添加条件</el-button
134
+ >
135
+ </div>
136
+
137
+ <div class="rule-section">
138
+ <div class="rule-section-title"><span>则设置字段/容器</span></div>
139
+ <div
140
+ v-for="(t, tIdx) in rule.targets"
141
+ :key="tIdx"
142
+ class="rule-row target-row"
143
+ >
144
+ <el-select
145
+ v-model="t.field"
146
+ filterable
147
+ size="mini"
148
+ placeholder="目标字段/容器"
149
+ class="cell-field"
150
+ >
151
+ <el-option
152
+ v-for="(n, idx) in treeOptions"
153
+ :key="n.value + '#' + idx"
154
+ :value="n.value"
155
+ :label="optPlainLabel(n)"
156
+ :disabled="n.isContainer && !n.hasName"
157
+ >
158
+ <span
159
+ :style="{ paddingLeft: n.depth * 14 + 'px' }"
160
+ :class="{ 'opt-container': n.isContainer }"
161
+ >
162
+ <span v-if="n.isContainer" class="opt-tag"
163
+ >[{{ containerTypeLabel(n.type) }}]</span
164
+ >{{ n.baseLabel }}
165
+ </span>
166
+ </el-option>
167
+ </el-select>
168
+ <span class="target-attr">
169
+ <span class="attr-label">显示</span>
170
+ <el-select
171
+ v-model="t.visible"
172
+ size="mini"
173
+ class="cell-tri"
174
+ clearable
175
+ placeholder="不变"
176
+ >
177
+ <el-option label="显示" :value="true"></el-option>
178
+ <el-option label="隐藏" :value="false"></el-option>
179
+ </el-select>
180
+ </span>
181
+ <span class="target-attr">
182
+ <span class="attr-label">必填</span>
183
+ <el-select
184
+ v-model="t.required"
185
+ size="mini"
186
+ class="cell-tri"
187
+ clearable
188
+ placeholder="不变"
189
+ >
190
+ <el-option label="必填" :value="true"></el-option>
191
+ <el-option label="非必填" :value="false"></el-option>
192
+ </el-select>
193
+ </span>
194
+ <span class="target-attr">
195
+ <span class="attr-label">只读</span>
196
+ <el-select
197
+ v-model="t.readonly"
198
+ size="mini"
199
+ class="cell-tri"
200
+ clearable
201
+ placeholder="不变"
202
+ >
203
+ <el-option label="只读" :value="true"></el-option>
204
+ <el-option label="可编辑" :value="false"></el-option>
205
+ </el-select>
206
+ </span>
207
+ <span class="target-attr">
208
+ <span class="attr-label">禁用</span>
209
+ <el-select
210
+ v-model="t.disabled"
211
+ size="mini"
212
+ class="cell-tri"
213
+ clearable
214
+ placeholder="不变"
215
+ >
216
+ <el-option label="禁用" :value="true"></el-option>
217
+ <el-option label="启用" :value="false"></el-option>
218
+ </el-select>
219
+ </span>
220
+ <el-button
221
+ type="text"
222
+ icon="el-icon-remove-outline"
223
+ class="danger-text row-remove"
224
+ aria-label="删除目标字段"
225
+ @click="removeTarget(rule, tIdx)"
226
+ ></el-button>
227
+ </div>
228
+ <el-button type="text" icon="el-icon-plus" @click="addTarget(rule)"
229
+ >添加目标字段</el-button
230
+ >
231
+ <div class="tip-text">
232
+ 目标为容器时仅「显示」生效,必填/只读/禁用将被忽略
233
+ </div>
234
+ </div>
235
+ </div>
236
+
237
+ <el-button
238
+ type="primary"
239
+ plain
240
+ icon="el-icon-plus"
241
+ size="mini"
242
+ @click="addRule"
243
+ style="width: 100%"
244
+ >
245
+ 添加规则
246
+ </el-button>
247
+ </div>
248
+
249
+ <div slot="footer" class="rule-dialog-footer">
250
+ <span class="rule-count">共 {{ editingRules.length }} 条规则</span>
251
+ <el-button size="mini" @click="ruleDialogVisible = false"
252
+ >取消</el-button
253
+ >
254
+ <el-button size="mini" type="primary" @click="saveRules"
255
+ >确定</el-button
256
+ >
257
+ </div>
258
+ </el-dialog>
259
+
260
+ <el-dialog
261
+ title="额外参数"
262
+ :visible.sync="scriptParamDialogVisible"
263
+ v-if="scriptParamDialogVisible"
264
+ :show-close="true"
265
+ custom-class="dialog-style list-dialog"
266
+ v-dialog-drag
267
+ :close-on-click-modal="false"
268
+ :close-on-press-escape="false"
269
+ :destroy-on-close="true"
270
+ :append-to-body="true"
271
+ :modal-append-to-body="true"
272
+ >
273
+ <div class="cont">
274
+ <code-editor
275
+ :mode="'javascript'"
276
+ :readonly="false"
277
+ v-model="editingScriptParam"
278
+ ref="scriptParamEditor"
279
+ ></code-editor>
280
+ </div>
281
+ <div slot="footer" class="dialog-footer">
282
+ <el-button
283
+ @click="scriptParamDialogVisible = false"
284
+ class="button-sty"
285
+ icon="el-icon-close"
286
+ >
287
+ {{ i18nt("designer.hint.cancel") }}
288
+ </el-button>
289
+ <el-button
290
+ type="primary"
291
+ @click="saveDynamicFieldScriptParam"
292
+ class="button-sty"
293
+ icon="el-icon-check"
294
+ >
295
+ {{ i18nt("designer.hint.confirm") }}
296
+ </el-button>
297
+ </div>
298
+ </el-dialog>
299
+ </div>
300
+ </template>
301
+
302
+ <script>
303
+ import { fieldControlTreeOptions, commitLocalFieldRules } from "@base/components/xform/form-render/fieldControlEngine";
304
+ import i18n from "../../../../components/xform/utils/i18n";
305
+
306
+ const NO_VALUE_OPERATORS = ["empty", "notEmpty"];
307
+
308
+ export default {
309
+ name: "form-dynamicField-setting",
310
+ mixins: [i18n],
311
+ props: {
312
+ designer: Object,
313
+ formConfig: Object,
314
+ },
315
+ data() {
316
+ return {
317
+ ruleDialogVisible: false,
318
+ scriptParamDialogVisible: false,
319
+ editingScriptParam: "",
320
+ editingRules: [],
321
+ operatorOptions: [
322
+ { label: "等于", value: "eq" },
323
+ { label: "不等于", value: "ne" },
324
+ { label: "大于", value: "gt" },
325
+ { label: "小于", value: "lt" },
326
+ { label: "大于等于", value: "ge" },
327
+ { label: "小于等于", value: "le" },
328
+ { label: "属于(逗号分隔)", value: "in" },
329
+ { label: "不属于(逗号分隔)", value: "nin" },
330
+ { label: "包含", value: "includes" },
331
+ { label: "为空", value: "empty" },
332
+ { label: "不为空", value: "notEmpty" },
333
+ ],
334
+ };
335
+ },
336
+ computed: {
337
+ fieldControlTreeState() {
338
+ const list = (this.designer && this.designer.widgetList) || [];
339
+ try {
340
+ return { options: fieldControlTreeOptions(list), error: "" };
341
+ } catch (error) {
342
+ return { options: [], error: error.message || "动态规则目标无效" };
343
+ }
344
+ },
345
+ // 带层级的字段/容器候选(整个表单)
346
+ treeOptions() {
347
+ return this.fieldControlTreeState.options;
348
+ },
349
+ },
350
+ methods: {
351
+ op_noValue(operator) {
352
+ return NO_VALUE_OPERATORS.indexOf(operator) > -1;
353
+ },
354
+ // 选中态/过滤用的纯文本标签
355
+ optPlainLabel(n) {
356
+ if (n.isContainer) {
357
+ return "[" + this.containerTypeLabel(n.type) + "] " + n.baseLabel;
358
+ }
359
+ return n.baseLabel;
360
+ },
361
+ containerTypeLabel(type) {
362
+ const map = {
363
+ grid: "栅格",
364
+ table: "表格布局",
365
+ "h5-table": "H5表格",
366
+ tab: "标签页",
367
+ "tab-pane": "标签项",
368
+ card: "卡片",
369
+ "vf-box": "区块",
370
+ panel: "面板",
371
+ detail: "单据详情",
372
+ "detail-plain": "单据详情(简洁)",
373
+ "detail-pane": "详情面板",
374
+ "detail-h5": "H5详情",
375
+ "h5-card": "H5卡片",
376
+ "h5-card-pane": "H5卡片面板",
377
+ "sub-form": "子表单",
378
+ "grid-sub-form": "网格子表单",
379
+ };
380
+ return map[type] || type;
381
+ },
382
+ genId() {
383
+ return "fdr_" + Date.now() + "_" + Math.floor(Math.random() * 10000);
384
+ },
385
+ editDynamicFieldScriptParam() {
386
+ this.editingScriptParam = this.formConfig.dynamicFieldScriptParam || "";
387
+ this.scriptParamDialogVisible = true;
388
+ },
389
+ saveDynamicFieldScriptParam() {
390
+ const codeHints = this.$refs.scriptParamEditor.getEditorAnnotations();
391
+ let syntaxErrorFlag = false;
392
+ if (codeHints && codeHints.length > 0) {
393
+ codeHints.forEach((chItem) => {
394
+ if (chItem.type === "error") {
395
+ syntaxErrorFlag = true;
396
+ }
397
+ });
398
+ if (syntaxErrorFlag) {
399
+ this.$message.error(
400
+ this.i18nt("designer.setting.syntaxCheckWarning")
401
+ );
402
+ return;
403
+ }
404
+ }
405
+ this.formConfig.dynamicFieldScriptParam = this.editingScriptParam;
406
+ this.scriptParamDialogVisible = false;
407
+ },
408
+ openRuleDialog() {
409
+ this.editingRules = JSON.parse(
410
+ JSON.stringify(this.formConfig.dynamicFieldRules || [])
411
+ );
412
+ this.editingRules.forEach((rule) => {
413
+ if (!rule.id) rule.id = this.genId();
414
+ if (!rule.logic) rule.logic = "and";
415
+ if (!rule.conditions) rule.conditions = [];
416
+ if (!rule.targets) rule.targets = [];
417
+ });
418
+ this.ruleDialogVisible = true;
419
+ },
420
+ addRule() {
421
+ this.editingRules.push({
422
+ id: this.genId(),
423
+ logic: "and",
424
+ conditions: [{ field: "", operator: "eq", value: "" }],
425
+ targets: [
426
+ {
427
+ field: "",
428
+ visible: undefined,
429
+ required: undefined,
430
+ readonly: undefined,
431
+ disabled: undefined,
432
+ },
433
+ ],
434
+ });
435
+ },
436
+ removeRule(idx) {
437
+ this.editingRules.splice(idx, 1);
438
+ },
439
+ addCondition(rule) {
440
+ rule.conditions.push({ field: "", operator: "eq", value: "" });
441
+ },
442
+ removeCondition(rule, idx) {
443
+ rule.conditions.splice(idx, 1);
444
+ },
445
+ addTarget(rule) {
446
+ rule.targets.push({
447
+ field: "",
448
+ visible: undefined,
449
+ required: undefined,
450
+ readonly: undefined,
451
+ disabled: undefined,
452
+ });
453
+ },
454
+ removeTarget(rule, idx) {
455
+ rule.targets.splice(idx, 1);
456
+ },
457
+ saveRules() {
458
+ try {
459
+ this.formConfig.dynamicFieldRules = commitLocalFieldRules(
460
+ this.editingRules, this.designer?.widgetList || []);
461
+ this.ruleDialogVisible = false;
462
+ } catch (error) { this.$message.error(error.message); }
463
+ },
464
+ },
465
+ };
466
+ </script>
467
+
468
+ <style lang="scss" scoped>
469
+ .tip-text {
470
+ font-size: 12px;
471
+ color: #909399;
472
+ line-height: 1.4;
473
+ margin-top: 2px;
474
+ }
475
+
476
+ .tree-options-error {
477
+ color: #f56c6c;
478
+ }
479
+
480
+ .rule-list {
481
+ max-height: 56vh;
482
+ overflow-y: auto;
483
+ }
484
+
485
+ .rule-card {
486
+ border: 1px solid #ebeef5;
487
+ border-radius: 4px;
488
+ padding: 10px 12px;
489
+ margin-bottom: 12px;
490
+ background: #fafafa;
491
+ }
492
+
493
+ .rule-card-header {
494
+ display: flex;
495
+ justify-content: space-between;
496
+ align-items: center;
497
+ margin-bottom: 6px;
498
+
499
+ .rule-title {
500
+ font-weight: bold;
501
+ font-size: 13px;
502
+ }
503
+ }
504
+
505
+ .rule-section {
506
+ margin-top: 6px;
507
+ }
508
+
509
+ .rule-section-title {
510
+ display: flex;
511
+ align-items: center;
512
+ gap: 8px;
513
+ font-size: 13px;
514
+ color: #606266;
515
+ margin-bottom: 6px;
516
+ }
517
+
518
+ .rule-row {
519
+ display: flex;
520
+ align-items: center;
521
+ gap: 6px;
522
+ margin-bottom: 6px;
523
+ flex-wrap: wrap;
524
+ }
525
+
526
+ .cell-field {
527
+ width: 180px;
528
+ }
529
+
530
+ .cell-op {
531
+ width: 120px;
532
+ }
533
+
534
+ .cell-value {
535
+ width: 130px;
536
+ }
537
+
538
+ .cell-tri {
539
+ width: 92px;
540
+ }
541
+
542
+ .target-attr {
543
+ display: flex;
544
+ align-items: center;
545
+ gap: 2px;
546
+
547
+ .attr-label {
548
+ font-size: 12px;
549
+ color: #909399;
550
+ }
551
+ }
552
+
553
+ .danger-text {
554
+ color: #f56c6c;
555
+ }
556
+
557
+ .opt-container {
558
+ color: #409eff;
559
+ }
560
+
561
+ .opt-tag {
562
+ color: #909399;
563
+ font-size: 12px;
564
+ margin-right: 4px;
565
+ }
566
+ </style>
567
+
568
+ <!-- 弹框挂载到 body,以独立类名隔离样式,并覆盖设置面板的控件宽度。 -->
569
+ <style lang="scss">
570
+ .el-dialog.dynamic-field-rule-dialog {
571
+ display: flex;
572
+ flex-direction: column;
573
+ max-width: calc(100vw - 32px);
574
+ max-height: 88vh;
575
+ margin-bottom: 0;
576
+ border-radius: 8px;
577
+
578
+ > .el-dialog__header {
579
+ flex: none;
580
+ padding: 20px 24px;
581
+ border-bottom: 1px solid #ebeef5;
582
+ }
583
+
584
+ > .el-dialog__body {
585
+ display: flex;
586
+ flex-direction: column;
587
+ min-height: 0;
588
+ padding: 16px 24px;
589
+ overflow: hidden;
590
+ }
591
+
592
+ > .el-dialog__footer {
593
+ flex: none;
594
+ padding: 14px 24px;
595
+ border-top: 1px solid #ebeef5;
596
+ }
597
+
598
+ .rule-dialog-summary {
599
+ flex: none;
600
+ margin-bottom: 14px;
601
+ color: #909399;
602
+ font-size: 13px;
603
+ line-height: 20px;
604
+ }
605
+
606
+ .rule-list {
607
+ min-height: 0;
608
+ max-height: none;
609
+ overflow-y: auto;
610
+ overflow-x: hidden;
611
+ padding-right: 4px;
612
+ }
613
+
614
+ .rule-empty {
615
+ padding: 40px 12px;
616
+ text-align: center;
617
+ color: #909399;
618
+ }
619
+
620
+ .rule-card {
621
+ padding: 0 16px 16px;
622
+ margin-bottom: 16px;
623
+ border: 1px solid #e4e7ed;
624
+ border-radius: 6px;
625
+ background: #fff;
626
+ }
627
+
628
+ .rule-card-header {
629
+ min-height: 44px;
630
+ margin: 0 -16px 14px;
631
+ padding: 0 16px;
632
+ background: #f5f7fa;
633
+ border-bottom: 1px solid #ebeef5;
634
+ border-radius: 6px 6px 0 0;
635
+ }
636
+
637
+ .rule-title { font-size: 14px; color: #303133; }
638
+ .rule-section + .rule-section { margin-top: 14px; padding-top: 14px; border-top: 1px dashed #e4e7ed; }
639
+ .rule-section-title { margin-bottom: 12px; gap: 12px; }
640
+
641
+ .rule-row {
642
+ display: grid;
643
+ gap: 10px;
644
+ margin-bottom: 10px;
645
+ min-width: 0;
646
+ }
647
+
648
+ .condition-row { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr) 28px; }
649
+ .target-row {
650
+ grid-template-columns: repeat(4, minmax(0, 1fr)) 28px;
651
+ padding: 12px;
652
+ background: #fafbfd;
653
+ border: 1px solid #ebeef5;
654
+ border-radius: 4px;
655
+ }
656
+
657
+ .target-row > .cell-field { grid-column: 1 / 5; }
658
+ .target-row > .row-remove { grid-column: 5; grid-row: 1; }
659
+ .target-attr { display: flex; flex-direction: column; align-items: stretch; gap: 6px; min-width: 0; }
660
+ .target-attr .attr-label { color: #606266; font-size: 12px; line-height: 18px; }
661
+
662
+ .rule-row .el-select,
663
+ .rule-row .el-input,
664
+ .rule-row .cell-field,
665
+ .rule-row .cell-op,
666
+ .rule-row .cell-value,
667
+ .rule-row .cell-tri { width: 100% !important; min-width: 0; margin: 0; }
668
+
669
+ .row-remove { width: 28px; margin: 0; padding: 8px 0; }
670
+ .rule-dialog-footer { display: flex; align-items: center; gap: 10px; }
671
+ .rule-dialog-footer .el-button { margin: 0; }
672
+ .rule-count { margin-right: auto; font-size: 13px; color: #909399; }
673
+
674
+ @media (max-width: 600px) {
675
+ > .el-dialog__body { padding: 12px; }
676
+ .condition-row { grid-template-columns: minmax(0, 1fr) 28px; }
677
+ .condition-row > .cell-field { grid-column: 1; }
678
+ .condition-row > .row-remove { grid-column: 2; grid-row: 1; }
679
+ .condition-row > .cell-op, .condition-row > .cell-value { grid-column: 1; }
680
+ .target-row { grid-template-columns: repeat(2, minmax(0, 1fr)) 28px; }
681
+ .target-row > .cell-field { grid-column: 1 / 3; }
682
+ .target-row > .row-remove { grid-column: 3; }
683
+ .target-attr:nth-of-type(odd) { grid-column: 1; }
684
+ .target-attr:nth-of-type(even) { grid-column: 2; }
685
+ }
686
+ }
687
+ </style>