cloud-web-corejs 1.0.54-dev.786 → 1.0.54-dev.787

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 (121) hide show
  1. package/package.json +2 -1
  2. package/src/components/VabUpload/index.vue +1 -1
  3. package/src/components/excelImport/index.js +69 -7
  4. package/src/components/excelImport/mixins.js +841 -9
  5. package/src/components/fileLibrary/fileHistoryDialog.vue +1 -1
  6. package/src/components/fileLibrary/fileObjAuthEdit.vue +1 -1
  7. package/src/components/fileLibrary/filterDialog.vue +1 -1
  8. package/src/components/formDialog/README.md +324 -241
  9. package/src/components/formDialog/actionButtons.js +90 -0
  10. package/src/components/formDialog/index.js +48 -35
  11. package/src/components/formDialog/index.vue +802 -724
  12. package/src/components/formDrawer/drawer.vue +719 -0
  13. package/src/components/formDrawer/index.js +50 -0
  14. package/src/components/onlineTalk/talkUserDialog.vue +1 -1
  15. package/src/components/tempStorage/tempStorageDialog.vue +1 -1
  16. package/src/components/wf/addTaskUserdialog.vue +1 -1
  17. package/src/components/xform/form-designer/form-widget/container-widget/data-table-widget.vue +1 -1
  18. package/src/components/xform/form-designer/form-widget/dialog/formDialog.vue +115 -4
  19. package/src/components/xform/form-designer/form-widget/dialog/formDrawer.vue +102 -5
  20. package/src/components/xform/form-designer/form-widget/dialog/formFieldDialog.vue +1 -1
  21. package/src/components/xform/form-designer/form-widget/dialog/vabSearchDialog.vue +1 -1
  22. package/src/components/xform/form-designer/setting-panel/action-button-config-dialog.vue +241 -0
  23. package/src/components/xform/form-designer/setting-panel/form-setting.vue +116 -0
  24. package/src/components/xform/form-designer/setting-panel/property-editor/container-detail/detail-editor.vue +6 -0
  25. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +1 -0
  26. package/src/components/xform/form-render/container-item/data-table-mixin.js +1460 -1460
  27. package/src/components/xform/form-render/container-item/detail-item.vue +11 -2
  28. package/src/components/xform/form-render/indexMixin.js +5 -0
  29. package/src/index.js +506 -472
  30. package/src/utils/lazyPlugin.js +66 -0
  31. package/src/views/bd/setting/bd_attach_setting/dialog.vue +1 -1
  32. package/src/views/bd/setting/bd_attach_setting/list.vue +1 -1
  33. package/src/views/bd/setting/bd_company_env/dialog.vue +1 -1
  34. package/src/views/bd/setting/bd_company_env/list.vue +1 -1
  35. package/src/views/bd/setting/formVersion/ftHistoryDialog.vue +1 -1
  36. package/src/views/bd/setting/form_import_log/list.vue +1 -1
  37. package/src/views/bd/setting/form_script/dialog.vue +1 -1
  38. package/src/views/bd/setting/form_script/form_list.vue +1 -1
  39. package/src/views/bd/setting/form_script/list.vue +1 -1
  40. package/src/views/bd/setting/form_script/list1.vue +1 -1
  41. package/src/views/bd/setting/form_template/dialog.vue +1 -1
  42. package/src/views/bd/setting/form_template/ftHistoryDialog.vue +1 -1
  43. package/src/views/bd/setting/form_template/itemList.vue +1 -1
  44. package/src/views/bd/setting/form_template/list.vue +1 -1
  45. package/src/views/bd/setting/form_template/wf_list.vue +1 -1
  46. package/src/views/bd/setting/logic_param/list.vue +1 -1
  47. package/src/views/bd/setting/menu_kind/dialog.vue +1 -1
  48. package/src/views/bd/setting/menu_kind/list.vue +1 -1
  49. package/src/views/bd/setting/push_data/list.vue +1 -1
  50. package/src/views/bd/setting/push_data_h/list.vue +1 -1
  51. package/src/views/bd/setting/request_async_setting/list.vue +1 -1
  52. package/src/views/bd/setting/request_setting/list.vue +1 -1
  53. package/src/views/bd/setting/table_model/dialog.vue +1 -1
  54. package/src/views/bd/setting/table_model/list.vue +1 -1
  55. package/src/views/bd/setting/table_model/zdDialog.vue +1 -1
  56. package/src/views/support/export_template/list.vue +1 -1
  57. package/src/views/user/access_log/list.vue +1 -1
  58. package/src/views/user/access_log/list2.vue +1 -1
  59. package/src/views/user/api_request/list.vue +1 -1
  60. package/src/views/user/area/dialog.vue +1 -1
  61. package/src/views/user/area/list.vue +1 -1
  62. package/src/views/user/bill_setting/list.vue +1 -1
  63. package/src/views/user/bill_setting/userDialog.vue +1 -1
  64. package/src/views/user/code_rules/list.vue +1 -1
  65. package/src/views/user/common_attribute/list.vue +1 -1
  66. package/src/views/user/common_script/list.vue +1 -1
  67. package/src/views/user/company_info/dialog.vue +1 -1
  68. package/src/views/user/country/dialog.vue +1 -1
  69. package/src/views/user/country/list.vue +1 -1
  70. package/src/views/user/data_type_setting/dialog.vue +1 -1
  71. package/src/views/user/extend_datasource/dialog.vue +1 -1
  72. package/src/views/user/extend_datasource/list.vue +1 -1
  73. package/src/views/user/fieldTranslation/list.vue +1 -1
  74. package/src/views/user/field_values_invisible/list.vue +1 -1
  75. package/src/views/user/file_type/list.vue +1 -1
  76. package/src/views/user/file_view_area/list.vue +1 -1
  77. package/src/views/user/form/form_template/dialog.vue +1 -1
  78. package/src/views/user/form/form_template/itemList.vue +1 -1
  79. package/src/views/user/form/form_template/list.vue +1 -1
  80. package/src/views/user/form/form_template_field/dialog.vue +1 -1
  81. package/src/views/user/form/form_type/dialog.vue +1 -1
  82. package/src/views/user/form/form_type/list.vue +1 -1
  83. package/src/views/user/form/report_requestaccess/dialog.vue +1 -1
  84. package/src/views/user/form/report_requestaccess/list.vue +1 -1
  85. package/src/views/user/groups/dialog.vue +1 -1
  86. package/src/views/user/groups/list.vue +1 -1
  87. package/src/views/user/lang_tag/dialog.vue +1 -1
  88. package/src/views/user/lang_tag/list.vue +1 -1
  89. package/src/views/user/language_setting/list.vue +1 -1
  90. package/src/views/user/menu/list.vue +1 -1
  91. package/src/views/user/mobile_menu/list.vue +1 -1
  92. package/src/views/user/notify_message/list.vue +1 -1
  93. package/src/views/user/notify_template/list.vue +1 -1
  94. package/src/views/user/notify_template/list2.vue +1 -1
  95. package/src/views/user/oplog/list.vue +1 -1
  96. package/src/views/user/position/dialog.vue +1 -1
  97. package/src/views/user/position/list.vue +1 -1
  98. package/src/views/user/project_tag/dialog.vue +1 -1
  99. package/src/views/user/project_tag/list.vue +1 -1
  100. package/src/views/user/push_setting/list.vue +1 -1
  101. package/src/views/user/request_setting/list.vue +1 -1
  102. package/src/views/user/role/dialog.vue +1 -1
  103. package/src/views/user/role/list.vue +1 -1
  104. package/src/views/user/sale_org/dialog.vue +1 -1
  105. package/src/views/user/sale_org/list.vue +1 -1
  106. package/src/views/user/system_notice/list.vue +1 -1
  107. package/src/views/user/system_parameter/list.vue +1 -1
  108. package/src/views/user/user/dialog.vue +1 -1
  109. package/src/views/user/user/form_dialog.vue +1 -1
  110. package/src/views/user/user/form_list.vue +1 -1
  111. package/src/views/user/user/list.vue +2 -2
  112. package/src/views/user/user_log_classify/list.vue +1 -1
  113. package/src/views/user/wf/wfReport/index.vue +2 -2
  114. package/src/views/user/wf/wf_auto_submit_data/list.vue +2 -2
  115. package/src/views/user/wf/wf_manage/list.vue +2 -2
  116. package/src/views/user/wf/wf_manage/list2.vue +2 -2
  117. package/src/views/user/wf/wf_obj_config/dialog.vue +1 -1
  118. package/src/views/user/wf/wf_obj_config/list.vue +1 -1
  119. package/src/views/user/wf/wf_obj_config/list_form.vue +1 -1
  120. package/src/views/user/wf/wf_transfer_setting/list.vue +1 -1
  121. package/src/views/user/workbench_menu/list.vue +1 -1
@@ -1,724 +1,802 @@
1
- <template>
2
- <el-dialog
3
- :title="title"
4
- :visible.sync="showWrap"
5
- :width="width"
6
- :close-on-click-modal="false"
7
- :append-to-body="true"
8
- :custom-class="xformMode ? 'dialog-style list-dialog' : 'dialog-style'"
9
- v-el-drag-dialog
10
- @close="handleClose"
11
- @closed="handleClosed"
12
- >
13
- <!-- xform 整表模式:按 formCode/formJson 渲染已配置的 xform 表单 -->
14
- <div
15
- v-if="xformMode"
16
- class="cont"
17
- v-loading="xformLoading"
18
- :style="xformReady ? '' : 'min-height: 120px'"
19
- >
20
- <v-form-render
21
- v-if="xformReady"
22
- ref="vFormRef"
23
- :form-json="xformJson"
24
- :form-data="xformFormData"
25
- :option-data="xformOptionData"
26
- :global-dsv="xformGlobalDsv"
27
- :report-template="xformTemplate"
28
- :dynamic-creation="true"
29
- @hook:mounted="handleXformMounted"
30
- />
31
- </div>
32
- <el-form
33
- v-else
34
- ref="form"
35
- :model="model"
36
- :rules="mergedRules"
37
- :label-width="labelWidth"
38
- :disabled="readonly"
39
- @submit.native.prevent
40
- >
41
- <el-row :gutter="16">
42
- <el-col
43
- v-for="field in visibleFields"
44
- :key="field.prop"
45
- :span="field.span || 24"
46
- >
47
- <el-form-item :label="field.label" :prop="field.prop">
48
- <field-render
49
- v-if="field.render"
50
- :field="field"
51
- :form-model="model"
52
- />
53
- <el-input
54
- v-else-if="!field.type || field.type === 'input'"
55
- v-model="model[field.prop]"
56
- v-bind="controlProps(field)"
57
- />
58
- <el-input
59
- v-else-if="field.type === 'textarea'"
60
- v-model="model[field.prop]"
61
- v-bind="controlProps(field)"
62
- />
63
- <el-input
64
- v-else-if="field.type === 'search'"
65
- class="search-input"
66
- v-model="model[field.prop]"
67
- v-el-readonly="searchReadonly(field)"
68
- v-bind="controlProps(field)"
69
- @clear="handleSearchClear(field)"
70
- >
71
- <i
72
- slot="suffix"
73
- class="el-input__icon el-icon-search"
74
- @click="handleSearch(field)"
75
- ></i>
76
- </el-input>
77
- <base-input-number
78
- v-else-if="field.type === 'number'"
79
- v-model="model[field.prop]"
80
- style="width: 100%"
81
- v-bind="controlProps(field)"
82
- />
83
- <el-select
84
- v-else-if="field.type === 'select'"
85
- v-model="model[field.prop]"
86
- style="width: 100%"
87
- :loading="optionsLoadingMap[field.prop]"
88
- v-bind="controlProps(field)"
89
- >
90
- <el-option
91
- v-for="opt in getOptions(field)"
92
- :key="opt.value"
93
- :label="opt.label"
94
- :value="opt.value"
95
- />
96
- </el-select>
97
- <el-date-picker
98
- v-else-if="
99
- field.type === 'date'
100
- || field.type === 'datetime'
101
- || field.type === 'daterange'
102
- "
103
- v-model="model[field.prop]"
104
- style="width: 100%"
105
- v-bind="controlProps(field)"
106
- />
107
- <el-switch
108
- v-else-if="field.type === 'switch'"
109
- v-model="model[field.prop]"
110
- v-bind="controlProps(field)"
111
- />
112
- <el-radio-group
113
- v-else-if="field.type === 'radio'"
114
- v-model="model[field.prop]"
115
- v-bind="controlProps(field)"
116
- >
117
- <el-radio
118
- v-for="opt in getOptions(field)"
119
- :key="opt.value"
120
- :label="opt.value"
121
- >{{ opt.label }}</el-radio
122
- >
123
- </el-radio-group>
124
- <el-checkbox-group
125
- v-else-if="field.type === 'checkbox'"
126
- v-model="model[field.prop]"
127
- v-bind="controlProps(field)"
128
- >
129
- <el-checkbox
130
- v-for="opt in getOptions(field)"
131
- :key="opt.value"
132
- :label="opt.value"
133
- >{{ opt.label }}</el-checkbox
134
- >
135
- </el-checkbox-group>
136
- </el-form-item>
137
- </el-col>
138
- </el-row>
139
- </el-form>
140
- <span slot="footer" class="dialog-footer">
141
- <template v-if="readonly">
142
- <el-button type="primary" class="button-sty" @click="showWrap = false"
143
- ><i class="el-icon-close el-icon"></i
144
- >{{ $t2("关 闭", "system.button.close2") }}</el-button
145
- >
146
- </template>
147
- <template v-else>
148
- <el-button
149
- type="primary"
150
- plain
151
- class="button-sty"
152
- @click="showWrap = false"
153
- ><i class="el-icon-close el-icon"></i
154
- >{{ $t2(" 消", "system.button.cancel2") }}</el-button
155
- >
156
- <el-button
157
- type="primary"
158
- class="button-sty"
159
- :loading="confirmLoading"
160
- @click="handleConfirm"
161
- ><i class="el-icon-check el-icon"></i
162
- >{{ $t2("确 定", "system.button.confirm2") }}</el-button
163
- >
164
- </template>
165
- </span>
166
- </el-dialog>
167
- </template>
168
-
169
- <script>
170
- import { openComponentDialog } from "@base/utils/componentDialog";
171
-
172
- // 自定义渲染逃生口:field.render(h, formModel, field) 返回 VNode
173
- const FieldRender = {
174
- functional: true,
175
- props: {
176
- field: { type: Object, required: true },
177
- formModel: { type: Object, required: true },
178
- },
179
- render(h, ctx) {
180
- return ctx.props.field.render(h, ctx.props.formModel, ctx.props.field);
181
- },
182
- };
183
-
184
- export default {
185
- name: "formDialog",
186
- components: {
187
- FieldRender,
188
- // xform 渲染器按需加载(独立 chunk),加载前先注册 xform 扩展控件
189
- VFormRender: () =>
190
- import("@base/components/xform/extension/extension-loader").then(
191
- (ext) => {
192
- ext.loadExtension();
193
- return import("@base/components/xform/form-render/index.vue");
194
- }
195
- ),
196
- },
197
- data() {
198
- return {
199
- title: "",
200
- width: "600px",
201
- labelWidth: "100px",
202
- fields: [],
203
- model: {},
204
- rules: {},
205
- readonly: false,
206
- showWrap: false,
207
- confirmLoading: false,
208
- // 词汇/脚本编码等远程数据源取回的下拉选项,按 field.prop 存放
209
- remoteOptionsMap: {},
210
- optionsLoadingMap: {},
211
- // xform 整表模式(formCode/formJson 二选一触发)
212
- xformMode: false,
213
- xformLoading: false,
214
- xformReady: false,
215
- xformJson: null,
216
- xformFormData: {},
217
- xformOptionData: {},
218
- xformGlobalDsv: {},
219
- xformTemplate: null,
220
- };
221
- },
222
- computed: {
223
- // 显隐联动:visible 为函数时按 model 动态计算,隐藏字段不渲染也不参与校验
224
- visibleFields() {
225
- return this.fields.filter((field) =>
226
- typeof field.visible === "function"
227
- ? !!field.visible(this.model)
228
- : field.visible !== false
229
- );
230
- },
231
- mergedRules() {
232
- const rules = {};
233
- this.fields.forEach((field) => {
234
- const list = [];
235
- if (field.required) {
236
- list.push({
237
- required: true,
238
- message: `${field.label}${this.$t2(
239
- "不能为空",
240
- "system.validate.notEmpty"
241
- )}`,
242
- trigger: ["blur", "change"],
243
- });
244
- }
245
- if (field.rules) {
246
- list.push(...[].concat(field.rules));
247
- }
248
- if (list.length) {
249
- rules[field.prop] = list;
250
- }
251
- });
252
- Object.keys(this.rules || {}).forEach((prop) => {
253
- rules[prop] = (rules[prop] || []).concat(this.rules[prop]);
254
- });
255
- return rules;
256
- },
257
- },
258
- watch: {
259
- // 联动入口:深度监听 model,diff 出变化字段后派发 onChange / dependsOn 级联
260
- model: {
261
- deep: true,
262
- handler() {
263
- this.handleModelChange();
264
- },
265
- },
266
- },
267
- methods: {
268
- open(options = {}) {
269
- if (options.formCode || options.formJson) {
270
- return this.openXform(options);
271
- }
272
- const { fields = [], model = {} } = options;
273
- const formModel = { ...model };
274
- fields.forEach((field) => {
275
- if (field.prop in formModel) return;
276
- formModel[field.prop] = this.defaultFieldValue(field);
277
- });
278
-
279
- this.title = options.title || "";
280
- this.width = options.width || "600px";
281
- this.labelWidth = options.labelWidth || "100px";
282
- this.fields = fields;
283
- this.model = formModel;
284
- this.rules = options.rules || {};
285
- this.readonly = options.readonly === true;
286
- this._beforeConfirm = options.beforeConfirm;
287
- this._settled = false;
288
- this._prevModel = this.$baseLodash.cloneDeep(formModel);
289
- this.showWrap = true;
290
- this.loadRemoteOptions();
291
-
292
- return new Promise((resolve) => {
293
- this._resolve = resolve;
294
- });
295
- },
296
- // xform 整表模式:加载模板后由 VFormRender 渲染,确认走 xform 校验并返回表单数据
297
- openXform(options) {
298
- this.xformMode = true;
299
- this.title = options.title || "";
300
- this.width = options.width || "1000px";
301
- this.readonly = options.readonly === true;
302
- this._beforeConfirm = options.beforeConfirm;
303
- this._onReady = options.onReady;
304
- this._settled = false;
305
- this.xformFormData = options.model || options.formData || {};
306
- this.xformOptionData = options.optionData || {};
307
- this.xformGlobalDsv = options.globalDsv || {};
308
- this.showWrap = true;
309
-
310
- if (options.formJson) {
311
- this.initXformJson(options.formJson, options.formCode);
312
- } else {
313
- this.loadXformTemplate(options.formCode);
314
- }
315
- return new Promise((resolve) => {
316
- this._resolve = resolve;
317
- });
318
- },
319
- // 与 vform/render.vue 同链路:formTemplate/getByFormCode 取模板 JSON
320
- loadXformTemplate(formCode) {
321
- this.xformLoading = true;
322
- this.$http({
323
- aes: true,
324
- url: USER_PREFIX + "/formTemplate/getByFormCode",
325
- method: "post",
326
- data: { stringOne: formCode },
327
- isLoading: false,
328
- success: (res) => {
329
- this.xformLoading = false;
330
- const template = res.objx || {};
331
- const formJson = template.formViewContent
332
- ? JSON.parse(template.formViewContent)
333
- : {};
334
- this.xformTemplate = template;
335
- if (!this.title) {
336
- this.title = template.name || template.formName || "";
337
- }
338
- this.initXformJson(formJson, formCode);
339
- },
340
- });
341
- },
342
- initXformJson(formJson, formCode) {
343
- if (!formJson || !formJson.formConfig) {
344
- this.$message.error(
345
- `${this.$t2("表单模板不存在或内容为空", "system.message.formTemplateEmpty")}: ${formCode || ""}`
346
- );
347
- return;
348
- }
349
- this.xformJson = formJson;
350
- this.xformReady = true;
351
- },
352
- // VFormRender 为异步组件,挂载时机不确定,用 hook:mounted 收口
353
- handleXformMounted() {
354
- const formRef = this.$refs.vFormRef;
355
- if (!formRef) return;
356
- if (this.readonly) {
357
- formRef.setReadMode(true);
358
- }
359
- if (typeof this._onReady === "function") {
360
- this._onReady(formRef, this);
361
- }
362
- },
363
- handleXformConfirm() {
364
- const formRef = this.$refs.vFormRef;
365
- if (!formRef) return;
366
- // validateForm 自带失败提示与滚动定位;通过后 getFormData(false) 直取数据
367
- formRef.validateForm((valid) => {
368
- if (!valid) return;
369
- this.confirmWithData(this.$baseLodash.cloneDeep(formRef.getFormData(false)));
370
- });
371
- },
372
- defaultFieldValue(field) {
373
- if (field.type === "checkbox" || field.type === "daterange") return [];
374
- if (field.type === "switch") return false;
375
- return null;
376
- },
377
- evalFieldFlag(value) {
378
- return typeof value === "function" ? !!value(this.model) : value;
379
- },
380
- getOptions(field) {
381
- // 选项来源优先级:远程结果 > options 函数(可依赖 model 做级联)> 静态数组
382
- const source
383
- = this.remoteOptionsMap[field.prop]
384
- || (typeof field.options === "function"
385
- ? field.options(this.model)
386
- : field.options)
387
- || [];
388
- return source.map((opt) =>
389
- opt !== null && typeof opt === "object"
390
- ? opt
391
- : { label: String(opt), value: opt }
392
- );
393
- },
394
- // 联动派发:对比上次快照,逐字段触发 onChange,并处理 dependsOn 级联
395
- handleModelChange() {
396
- const prev = this._prevModel || {};
397
- const current = this.model;
398
- const keys = new Set([...Object.keys(current), ...Object.keys(prev)]);
399
- const changedKeys = [];
400
- keys.forEach((key) => {
401
- if (!this.$baseLodash.isEqual(current[key], prev[key])) {
402
- changedKeys.push(key);
403
- }
404
- });
405
- if (!changedKeys.length) return;
406
- this._prevModel = this.$baseLodash.cloneDeep(current);
407
-
408
- changedKeys.forEach((key) => {
409
- const field = this.fields.find((item) => item.prop === key);
410
- if (field && typeof field.onChange === "function") {
411
- field.onChange(current[key], current, field, this);
412
- }
413
- // dependsOn:依赖字段变化时清空自身值并刷新远程选项
414
- this.fields.forEach((item) => {
415
- if (Array.isArray(item.dependsOn) && item.dependsOn.includes(key)) {
416
- if (
417
- !this.$baseLodash.isEqual(
418
- current[item.prop],
419
- this.defaultFieldValue(item)
420
- )
421
- ) {
422
- this.$set(this.model, item.prop, this.defaultFieldValue(item));
423
- }
424
- this.reloadFieldOptions(item.prop);
425
- }
426
- });
427
- });
428
- },
429
- // onChange 回调使用:手动覆盖某字段选项
430
- setFieldOptions(prop, options) {
431
- this.$set(this.remoteOptionsMap, prop, options || []);
432
- },
433
- // 供 onChange 回调使用:按最新 model 重新拉取词汇/脚本选项
434
- reloadFieldOptions(prop) {
435
- const field = this.fields.find((item) => item.prop === prop);
436
- if (!field) return;
437
- if (field.dictCode) {
438
- this.fetchDictOptions(field);
439
- } else if (field.scriptCode) {
440
- this.fetchScriptOptions(field);
441
- }
442
- },
443
- loadRemoteOptions() {
444
- this.remoteOptionsMap = {};
445
- this.optionsLoadingMap = {};
446
- this.fields.forEach((field) => {
447
- if (field.dictCode) {
448
- this.fetchDictOptions(field);
449
- } else if (field.scriptCode) {
450
- this.fetchScriptOptions(field);
451
- }
452
- });
453
- },
454
- // 词汇:/user/common_attribute/listItems,显示 value、取值 sn(与 xform 约定一致)
455
- fetchDictOptions(field) {
456
- this.$set(this.optionsLoadingMap, field.prop, true);
457
- this.$getBaseDicts({
458
- code: field.dictCode,
459
- data:
460
- typeof field.dictData === "function"
461
- ? field.dictData(this.model)
462
- : field.dictData,
463
- success: ({ dicts }) => {
464
- this.$set(this.optionsLoadingMap, field.prop, false);
465
- this.$set(
466
- this.remoteOptionsMap,
467
- field.prop,
468
- dicts.map((item) => ({ label: item.value, value: item.sn }))
469
- );
470
- },
471
- });
472
- },
473
- // 脚本编码:"formCode/scriptCode",先取服务名再请求 bd_api 脚本接口
474
- fetchScriptOptions(field) {
475
- const parts = String(field.scriptCode)
476
- .split("/")
477
- .filter((item) => item);
478
- if (parts.length < 2) {
479
- console.warn(
480
- `[formDialog] scriptCode 需为 "formCode/scriptCode" 格式:${field.scriptCode}`
481
- );
482
- return;
483
- }
484
- const [formCode, scriptCode] = parts;
485
- this.$set(this.optionsLoadingMap, field.prop, true);
486
- this.$http({
487
- aes: true,
488
- url: USER_PREFIX + "/formScript/getServiceName",
489
- method: "post",
490
- data: { formCode, scriptCode },
491
- isLoading: false,
492
- success: (res) => {
493
- const serviceName = res.objx;
494
- if (!serviceName) {
495
- this.$set(this.optionsLoadingMap, field.prop, false);
496
- this.$baseAlert("服务名不存在");
497
- return;
498
- }
499
- this.$http({
500
- url: `/${serviceName}/bd_api/${formCode}/${scriptCode}`,
501
- method: "post",
502
- data: {
503
- ...(typeof field.scriptData === "function"
504
- ? field.scriptData(this.model)
505
- : field.scriptData),
506
- },
507
- isLoading: false,
508
- success: (res2) => {
509
- this.$set(this.optionsLoadingMap, field.prop, false);
510
- let options;
511
- if (typeof field.parseOptions === "function") {
512
- options = field.parseOptions(res2) || [];
513
- } else {
514
- const objx = res2.objx;
515
- const rows = Array.isArray(objx)
516
- ? objx
517
- : (objx && objx.records) || [];
518
- const labelField = field.labelField || "label";
519
- const valueField = field.valueField || "value";
520
- options = rows.map((row) => ({
521
- label: row[labelField],
522
- value: row[valueField],
523
- row,
524
- }));
525
- }
526
- this.$set(this.remoteOptionsMap, field.prop, options);
527
- },
528
- });
529
- },
530
- });
531
- },
532
- // 搜索框默认禁止手输(值由选择回填),传 props: { editable: true } 可放开
533
- searchReadonly(field) {
534
- return !(field.props && field.props.editable === true);
535
- },
536
- handleSearch(field) {
537
- if (this.readonly || this.evalFieldFlag(field.disabled)) return;
538
- if (typeof field.onSearch === "function") {
539
- field.onSearch(this.model, field);
540
- } else if (field.searchComponent) {
541
- this.openSearchComponent(field);
542
- } else if (field.searchDialog) {
543
- this.openXformSearchDialog(field);
544
- }
545
- },
546
- // 方式2:按路径打开自定义 Vue 弹框组件,组件 $emit("confirm", rows) 回写
547
- openSearchComponent(field) {
548
- const cfg = field.searchComponent;
549
- const events = cfg.events || {};
550
- openComponentDialog(
551
- {
552
- wrapper: false,
553
- ...cfg,
554
- events: {
555
- ...events,
556
- confirm: (...args) => {
557
- if (typeof events.confirm === "function") {
558
- events.confirm(...args);
559
- }
560
- this.applySearchResult(field, args[0], cfg);
561
- },
562
- },
563
- },
564
- this
565
- );
566
- },
567
- // 方式3:打开 xform 搜索弹框(searchFormDialog,按 formCode 渲染目标表单的数据表格)
568
- openXformSearchDialog(field) {
569
- const cfg = field.searchDialog;
570
- const { layoutType, fieldMap, onConfirm, showField, ...option } = cfg;
571
- openComponentDialog(
572
- {
573
- componentPath:
574
- "components/xform/form-designer/form-widget/dialog/searchFormDialog.vue",
575
- wrapper: false,
576
- // searchFormDialog 在 xform 环境外运行需要注入 getFormConfig
577
- provide: {
578
- getFormConfig: () => ({ layoutType: layoutType || "PC" }),
579
- },
580
- props: {
581
- option: {
582
- multiple: false,
583
- ...option,
584
- rows:
585
- typeof cfg.rows === "function" ? cfg.rows(this.model) : cfg.rows,
586
- confirm: (rows, formRef, dialogVm) => {
587
- if (typeof cfg.confirm === "function") {
588
- return cfg.confirm(rows, formRef, dialogVm);
589
- }
590
- this.applySearchResult(field, rows, cfg);
591
- },
592
- },
593
- },
594
- },
595
- this
596
- );
597
- },
598
- // 选中结果回写 model:onConfirm 完全自定义 > fieldMap 逐字段映射 > 默认回写本字段
599
- applySearchResult(field, rows, cfg) {
600
- const list = Array.isArray(rows) ? rows : rows ? [rows] : [];
601
- if (typeof cfg.onConfirm === "function") {
602
- cfg.onConfirm(this.model, list, field);
603
- return;
604
- }
605
- if (!list.length) return;
606
- const row = list[0];
607
- if (cfg.fieldMap) {
608
- Object.keys(cfg.fieldMap).forEach((modelKey) => {
609
- const source = cfg.fieldMap[modelKey];
610
- this.$set(
611
- this.model,
612
- modelKey,
613
- typeof source === "function" ? source(row, list) : row[source]
614
- );
615
- });
616
- } else {
617
- this.$set(this.model, field.prop, row[cfg.showField || field.prop]);
618
- }
619
- },
620
- handleSearchClear(field) {
621
- if (this.readonly || this.evalFieldFlag(field.disabled)) return;
622
- if (typeof field.onClear === "function") {
623
- field.onClear(this.model, field);
624
- } else {
625
- this.model[field.prop] = null;
626
- }
627
- },
628
- controlProps(field) {
629
- const defaults = {
630
- placeholder: field.placeholder,
631
- disabled: this.evalFieldFlag(field.disabled),
632
- clearable: true,
633
- };
634
- switch (field.type) {
635
- case "textarea":
636
- Object.assign(defaults, { type: "textarea", rows: 3 });
637
- break;
638
- case "search":
639
- // 搜索框与 xform vabsearch 同款样式:suffix 搜索图标 + clearable;
640
- // 只读用 v-el-readonly(原生 readonly,不影响清除图标),不走 readonly prop
641
- defaults.clearable = field.clearable !== false;
642
- break;
643
- case "number":
644
- // baseInputNumber:scale 控制小数位(v-limit-number),negative 放开负号输入
645
- Object.assign(defaults, {
646
- scale: field.scale != null ? field.scale : 2,
647
- negative: field.negative,
648
- controls: false,
649
- });
650
- break;
651
- case "date":
652
- Object.assign(defaults, { type: "date", valueFormat: "yyyy-MM-dd" });
653
- break;
654
- case "datetime":
655
- Object.assign(defaults, {
656
- type: "datetime",
657
- valueFormat: "yyyy-MM-dd HH:mm:ss",
658
- });
659
- break;
660
- case "daterange":
661
- Object.assign(defaults, {
662
- type: "daterange",
663
- valueFormat: "yyyy-MM-dd",
664
- });
665
- break;
666
- }
667
- const merged = { ...defaults, ...field.props };
668
- if (field.type === "search") {
669
- // editable 只用于 searchReadonly 判断,不透传给 el-input
670
- delete merged.editable;
671
- }
672
- return merged;
673
- },
674
- settle(value) {
675
- if (this._settled) return;
676
- this._settled = true;
677
- if (this._resolve) {
678
- this._resolve(value);
679
- }
680
- },
681
- handleConfirm() {
682
- if (this.xformMode) {
683
- this.handleXformConfirm();
684
- return;
685
- }
686
- this.$refs.form.validate((valid) => {
687
- if (!valid) return;
688
- this.confirmWithData({ ...this.model });
689
- });
690
- },
691
- confirmWithData(data) {
692
- if (typeof this._beforeConfirm === "function") {
693
- this.confirmLoading = true;
694
- Promise.resolve()
695
- .then(() => this._beforeConfirm(data))
696
- .then(() => {
697
- this.confirmLoading = false;
698
- this.settle(data);
699
- this.showWrap = false;
700
- })
701
- .catch(() => {
702
- // beforeConfirm 失败时不关闭弹框,由调用方自行提示错误
703
- this.confirmLoading = false;
704
- });
705
- } else {
706
- this.settle(data);
707
- this.showWrap = false;
708
- }
709
- },
710
- handleClose() {
711
- // 取消/右上角关闭:resolve(null);确认路径已先 settle,此处不生效
712
- this.settle(null);
713
- },
714
- handleClosed() {
715
- this.$destroy();
716
- if (this.$el && this.$el.parentNode) {
717
- this.$el.parentNode.removeChild(this.$el);
718
- }
719
- },
720
- },
721
- };
722
- </script>
723
-
724
- <style></style>
1
+ <template>
2
+ <el-dialog
3
+ :title="title"
4
+ :visible.sync="showWrap"
5
+ :width="width"
6
+ :close-on-click-modal="false"
7
+ :append-to-body="true"
8
+ :custom-class="xformMode ? 'dialog-style list-dialog' : 'dialog-style'"
9
+ v-el-drag-dialog
10
+ @close="handleClose"
11
+ @closed="handleClosed"
12
+ >
13
+ <!-- xform 整表模式:按 formCode/formJson 渲染已配置的 xform 表单 -->
14
+ <div
15
+ v-if="xformMode"
16
+ class="cont"
17
+ v-loading="xformLoading"
18
+ :style="xformReady ? '' : 'min-height: 120px'"
19
+ >
20
+ <v-form-render
21
+ v-if="xformReady"
22
+ ref="vFormRef"
23
+ :form-json="xformJson"
24
+ :form-data="xformFormData"
25
+ :option-data="xformOptionData"
26
+ :global-dsv="xformGlobalDsv"
27
+ :report-template="xformTemplate"
28
+ :dynamic-creation="true"
29
+ @hook:mounted="handleXformMounted"
30
+ />
31
+ </div>
32
+ <el-form
33
+ v-else
34
+ ref="form"
35
+ :model="model"
36
+ :rules="mergedRules"
37
+ :label-width="labelWidth"
38
+ :disabled="readonly"
39
+ @submit.native.prevent
40
+ >
41
+ <el-row :gutter="16">
42
+ <el-col
43
+ v-for="field in visibleFields"
44
+ :key="field.prop"
45
+ :span="field.span || 24"
46
+ >
47
+ <el-form-item :label="field.label" :prop="field.prop">
48
+ <field-render
49
+ v-if="field.render"
50
+ :field="field"
51
+ :form-model="model"
52
+ />
53
+ <el-input
54
+ v-else-if="!field.type || field.type === 'input'"
55
+ v-model="model[field.prop]"
56
+ v-bind="controlProps(field)"
57
+ />
58
+ <el-input
59
+ v-else-if="field.type === 'textarea'"
60
+ v-model="model[field.prop]"
61
+ v-bind="controlProps(field)"
62
+ />
63
+ <el-input
64
+ v-else-if="field.type === 'search'"
65
+ class="search-input"
66
+ v-model="model[field.prop]"
67
+ v-el-readonly="searchReadonly(field)"
68
+ v-bind="controlProps(field)"
69
+ @clear="handleSearchClear(field)"
70
+ >
71
+ <i
72
+ slot="suffix"
73
+ class="el-input__icon el-icon-search"
74
+ @click="handleSearch(field)"
75
+ ></i>
76
+ </el-input>
77
+ <base-input-number
78
+ v-else-if="field.type === 'number'"
79
+ v-model="model[field.prop]"
80
+ style="width: 100%"
81
+ v-bind="controlProps(field)"
82
+ />
83
+ <el-select
84
+ v-else-if="field.type === 'select'"
85
+ v-model="model[field.prop]"
86
+ style="width: 100%"
87
+ :loading="optionsLoadingMap[field.prop]"
88
+ v-bind="controlProps(field)"
89
+ >
90
+ <el-option
91
+ v-for="opt in getOptions(field)"
92
+ :key="opt.value"
93
+ :label="opt.label"
94
+ :value="opt.value"
95
+ />
96
+ </el-select>
97
+ <el-date-picker
98
+ v-else-if="
99
+ field.type === 'date'
100
+ || field.type === 'datetime'
101
+ || field.type === 'daterange'
102
+ "
103
+ v-model="model[field.prop]"
104
+ style="width: 100%"
105
+ v-bind="controlProps(field)"
106
+ />
107
+ <el-switch
108
+ v-else-if="field.type === 'switch'"
109
+ v-model="model[field.prop]"
110
+ v-bind="controlProps(field)"
111
+ />
112
+ <el-radio-group
113
+ v-else-if="field.type === 'radio'"
114
+ v-model="model[field.prop]"
115
+ v-bind="controlProps(field)"
116
+ >
117
+ <el-radio
118
+ v-for="opt in getOptions(field)"
119
+ :key="opt.value"
120
+ :label="opt.value"
121
+ >{{ opt.label }}</el-radio
122
+ >
123
+ </el-radio-group>
124
+ <el-checkbox-group
125
+ v-else-if="field.type === 'checkbox'"
126
+ v-model="model[field.prop]"
127
+ v-bind="controlProps(field)"
128
+ >
129
+ <el-checkbox
130
+ v-for="opt in getOptions(field)"
131
+ :key="opt.value"
132
+ :label="opt.value"
133
+ >{{ opt.label }}</el-checkbox
134
+ >
135
+ </el-checkbox-group>
136
+ </el-form-item>
137
+ </el-col>
138
+ </el-row>
139
+ </el-form>
140
+ <span slot="footer" class="dialog-footer">
141
+ <template v-if="readonly">
142
+ <el-button type="primary" class="button-sty" @click="showWrap = false"
143
+ ><i class="el-icon-close el-icon"></i
144
+ >{{ $t2("关 闭", "system.button.close2") }}</el-button
145
+ >
146
+ </template>
147
+ <!-- 自定义按钮(formConfig.actionButtons / options.buttons):固定取消 + 配置按钮,resolve {action, data} -->
148
+ <template v-else-if="visibleFooterButtons">
149
+ <el-button
150
+ type="primary"
151
+ plain
152
+ class="button-sty"
153
+ @click="showWrap = false"
154
+ ><i class="el-icon-close el-icon"></i
155
+ >{{ $t2("取 消", "system.button.cancel2") }}</el-button
156
+ >
157
+ <el-button
158
+ v-for="btn in visibleFooterButtons"
159
+ :key="btn.code"
160
+ :type="btn.type || 'primary'"
161
+ :plain="btn.plain === true"
162
+ class="button-sty"
163
+ :loading="!!btnLoadingMap[btn.code]"
164
+ @click="handleActionButton(btn)"
165
+ ><i v-if="btn.icon" :class="[btn.icon, 'el-icon']"></i
166
+ >{{ btnLabel(btn) }}</el-button
167
+ >
168
+ </template>
169
+ <template v-else>
170
+ <el-button
171
+ type="primary"
172
+ plain
173
+ class="button-sty"
174
+ @click="showWrap = false"
175
+ ><i class="el-icon-close el-icon"></i
176
+ >{{ $t2("取 消", "system.button.cancel2") }}</el-button
177
+ >
178
+ <el-button
179
+ type="primary"
180
+ class="button-sty"
181
+ :loading="confirmLoading"
182
+ @click="handleConfirm"
183
+ ><i class="el-icon-check el-icon"></i
184
+ >{{ $t2("确 定", "system.button.confirm2") }}</el-button
185
+ >
186
+ </template>
187
+ </span>
188
+ </el-dialog>
189
+ </template>
190
+
191
+ <script>
192
+ import { openComponentDialog } from "@base/utils/componentDialog";
193
+ import {
194
+ resolveActionButtons,
195
+ hasButtonPermission,
196
+ isButtonVisible,
197
+ runActionButton,
198
+ } from "./actionButtons";
199
+
200
+ // 自定义渲染逃生口:field.render(h, formModel, field) 返回 VNode
201
+ const FieldRender = {
202
+ functional: true,
203
+ props: {
204
+ field: { type: Object, required: true },
205
+ formModel: { type: Object, required: true },
206
+ },
207
+ render(h, ctx) {
208
+ return ctx.props.field.render(h, ctx.props.formModel, ctx.props.field);
209
+ },
210
+ };
211
+
212
+ export default {
213
+ name: "formDialog",
214
+ components: {
215
+ FieldRender,
216
+ // xform 渲染器按需加载(独立 chunk),加载前先注册 xform 扩展控件
217
+ VFormRender: () =>
218
+ import("@base/components/xform/extension/extension-loader").then(
219
+ (ext) => {
220
+ ext.loadExtension();
221
+ return import("@base/components/xform/form-render/index.vue");
222
+ }
223
+ ),
224
+ },
225
+ data() {
226
+ return {
227
+ title: "",
228
+ width: "600px",
229
+ labelWidth: "100px",
230
+ fields: [],
231
+ model: {},
232
+ rules: {},
233
+ readonly: false,
234
+ showWrap: false,
235
+ confirmLoading: false,
236
+ // 词汇/脚本编码等远程数据源取回的下拉选项,按 field.prop 存放
237
+ remoteOptionsMap: {},
238
+ optionsLoadingMap: {},
239
+ // xform 整表模式(formCode/formJson 二选一触发)
240
+ xformMode: false,
241
+ xformLoading: false,
242
+ xformReady: false,
243
+ xformJson: null,
244
+ xformFormData: {},
245
+ xformOptionData: {},
246
+ xformGlobalDsv: {},
247
+ xformTemplate: null,
248
+ // 自定义按钮(模板 formConfig.actionButtons / 调用方 options.buttons,权限过滤后)
249
+ customButtons: null,
250
+ btnLoadingMap: {},
251
+ formRefMounted: false,
252
+ };
253
+ },
254
+ computed: {
255
+ // 自定义按钮渲染列表:权限已在 initCustomButtons 过滤,此处按 visible 脚本过滤
256
+ //(依赖 formRefMounted:表单挂载后重算一次;不追踪 formData 变化)
257
+ visibleFooterButtons() {
258
+ if (!this.xformMode || !this.customButtons || !this.customButtons.length) {
259
+ return null;
260
+ }
261
+ this.formRefMounted; // eslint-disable-line no-unused-expressions
262
+ const buttons = this.customButtons.filter((btn) =>
263
+ isButtonVisible(btn, this.$refs.vFormRef)
264
+ );
265
+ return buttons.length ? buttons : null;
266
+ },
267
+ // 显隐联动:visible 为函数时按 model 动态计算,隐藏字段不渲染也不参与校验
268
+ visibleFields() {
269
+ return this.fields.filter((field) =>
270
+ typeof field.visible === "function"
271
+ ? !!field.visible(this.model)
272
+ : field.visible !== false
273
+ );
274
+ },
275
+ mergedRules() {
276
+ const rules = {};
277
+ this.fields.forEach((field) => {
278
+ const list = [];
279
+ if (field.required) {
280
+ list.push({
281
+ required: true,
282
+ message: `${field.label}${this.$t2(
283
+ "不能为空",
284
+ "system.validate.notEmpty"
285
+ )}`,
286
+ trigger: ["blur", "change"],
287
+ });
288
+ }
289
+ if (field.rules) {
290
+ list.push(...[].concat(field.rules));
291
+ }
292
+ if (list.length) {
293
+ rules[field.prop] = list;
294
+ }
295
+ });
296
+ Object.keys(this.rules || {}).forEach((prop) => {
297
+ rules[prop] = (rules[prop] || []).concat(this.rules[prop]);
298
+ });
299
+ return rules;
300
+ },
301
+ },
302
+ watch: {
303
+ // 联动入口:深度监听 model,diff 出变化字段后派发 onChange / dependsOn 级联
304
+ model: {
305
+ deep: true,
306
+ handler() {
307
+ this.handleModelChange();
308
+ },
309
+ },
310
+ },
311
+ methods: {
312
+ open(options = {}) {
313
+ if (options.formCode || options.formJson) {
314
+ return this.openXform(options);
315
+ }
316
+ const { fields = [], model = {} } = options;
317
+ const formModel = { ...model };
318
+ fields.forEach((field) => {
319
+ if (field.prop in formModel) return;
320
+ formModel[field.prop] = this.defaultFieldValue(field);
321
+ });
322
+
323
+ this.title = options.title || "";
324
+ this.width = options.width || "600px";
325
+ this.labelWidth = options.labelWidth || "100px";
326
+ this.fields = fields;
327
+ this.model = formModel;
328
+ this.rules = options.rules || {};
329
+ this.readonly = options.readonly === true;
330
+ this._beforeConfirm = options.beforeConfirm;
331
+ this._settled = false;
332
+ this._prevModel = this.$baseLodash.cloneDeep(formModel);
333
+ this.showWrap = true;
334
+ this.loadRemoteOptions();
335
+
336
+ return new Promise((resolve) => {
337
+ this._resolve = resolve;
338
+ });
339
+ },
340
+ // xform 整表模式:加载模板后由 VFormRender 渲染,确认走 xform 校验并返回表单数据
341
+ openXform(options) {
342
+ this.xformMode = true;
343
+ this.title = options.title || "";
344
+ this.width = options.width || "1000px";
345
+ this.readonly = options.readonly === true;
346
+ this._beforeConfirm = options.beforeConfirm;
347
+ this._onReady = options.onReady;
348
+ this._settled = false;
349
+ this._callerButtons = Array.isArray(options.buttons)
350
+ ? options.buttons
351
+ : null;
352
+ this._formCode = options.formCode || null;
353
+ this.customButtons = null;
354
+ this.btnLoadingMap = {};
355
+ this.formRefMounted = false;
356
+ this.xformFormData = options.model || options.formData || {};
357
+ this.xformOptionData = options.optionData || {};
358
+ this.xformGlobalDsv = options.globalDsv || {};
359
+ this.showWrap = true;
360
+
361
+ if (options.formJson) {
362
+ this.initXformJson(options.formJson, options.formCode);
363
+ } else {
364
+ this.loadXformTemplate(options.formCode);
365
+ }
366
+ return new Promise((resolve) => {
367
+ this._resolve = resolve;
368
+ });
369
+ },
370
+ // 与 vform/render.vue 同链路:formTemplate/getByFormCode 取模板 JSON
371
+ loadXformTemplate(formCode) {
372
+ this.xformLoading = true;
373
+ this.$http({
374
+ aes: true,
375
+ url: USER_PREFIX + "/formTemplate/getByFormCode",
376
+ method: "post",
377
+ data: { stringOne: formCode },
378
+ isLoading: false,
379
+ success: (res) => {
380
+ this.xformLoading = false;
381
+ const template = res.objx || {};
382
+ const formJson = template.formViewContent
383
+ ? JSON.parse(template.formViewContent)
384
+ : {};
385
+ this.xformTemplate = template;
386
+ if (!this.title) {
387
+ this.title = template.name || template.formName || "";
388
+ }
389
+ this.initXformJson(formJson, formCode);
390
+ },
391
+ });
392
+ },
393
+ initXformJson(formJson, formCode) {
394
+ if (!formJson || !formJson.formConfig) {
395
+ this.$message.error(
396
+ `${this.$t2("表单模板不存在或内容为空", "system.message.formTemplateEmpty")}: ${formCode || ""}`
397
+ );
398
+ return;
399
+ }
400
+ this.xformJson = formJson;
401
+ this.initCustomButtons(formJson.formConfig, formCode || this._formCode);
402
+ this.xformReady = true;
403
+ },
404
+ // 自定义按钮:调用方 buttons 覆盖模板 actionButtons;权限(formCode:authName)在此一次性过滤
405
+ initCustomButtons(formConfig, formCode) {
406
+ const buttons = resolveActionButtons(this._callerButtons, formConfig);
407
+ this.customButtons = buttons
408
+ ? buttons.filter((btn) => hasButtonPermission(btn, formCode, this))
409
+ : null;
410
+ },
411
+ // VFormRender 为异步组件,挂载时机不确定,用 hook:mounted 收口
412
+ handleXformMounted() {
413
+ const formRef = this.$refs.vFormRef;
414
+ if (!formRef) return;
415
+ if (this.readonly) {
416
+ formRef.setReadMode(true);
417
+ }
418
+ this.formRefMounted = true;
419
+ if (typeof this._onReady === "function") {
420
+ this._onReady(formRef, this);
421
+ }
422
+ },
423
+ btnLabel(btn) {
424
+ return this.$t1 ? this.$t1(btn.label) : btn.label;
425
+ },
426
+ handleActionButton(btn) {
427
+ const formRef = this.$refs.vFormRef;
428
+ if (!formRef || this.btnLoadingMap[btn.code]) return;
429
+ this.$set(this.btnLoadingMap, btn.code, true);
430
+ const clearLoading = () => this.$set(this.btnLoadingMap, btn.code, false);
431
+ runActionButton(btn, {
432
+ formRef,
433
+ host: this,
434
+ getSelection: () => [],
435
+ onSettle: (payload, close) => {
436
+ this.settle(payload);
437
+ if (close) this.showWrap = false;
438
+ },
439
+ }).then(clearLoading, clearLoading);
440
+ },
441
+ handleXformConfirm() {
442
+ const formRef = this.$refs.vFormRef;
443
+ if (!formRef) return;
444
+ // validateForm 自带失败提示与滚动定位;通过后 getFormData(false) 直取数据
445
+ formRef.validateForm((valid) => {
446
+ if (!valid) return;
447
+ this.confirmWithData(this.$baseLodash.cloneDeep(formRef.getFormData(false)));
448
+ });
449
+ },
450
+ defaultFieldValue(field) {
451
+ if (field.type === "checkbox" || field.type === "daterange") return [];
452
+ if (field.type === "switch") return false;
453
+ return null;
454
+ },
455
+ evalFieldFlag(value) {
456
+ return typeof value === "function" ? !!value(this.model) : value;
457
+ },
458
+ getOptions(field) {
459
+ // 选项来源优先级:远程结果 > options 函数(可依赖 model 做级联)> 静态数组
460
+ const source
461
+ = this.remoteOptionsMap[field.prop]
462
+ || (typeof field.options === "function"
463
+ ? field.options(this.model)
464
+ : field.options)
465
+ || [];
466
+ return source.map((opt) =>
467
+ opt !== null && typeof opt === "object"
468
+ ? opt
469
+ : { label: String(opt), value: opt }
470
+ );
471
+ },
472
+ // 联动派发:对比上次快照,逐字段触发 onChange,并处理 dependsOn 级联
473
+ handleModelChange() {
474
+ const prev = this._prevModel || {};
475
+ const current = this.model;
476
+ const keys = new Set([...Object.keys(current), ...Object.keys(prev)]);
477
+ const changedKeys = [];
478
+ keys.forEach((key) => {
479
+ if (!this.$baseLodash.isEqual(current[key], prev[key])) {
480
+ changedKeys.push(key);
481
+ }
482
+ });
483
+ if (!changedKeys.length) return;
484
+ this._prevModel = this.$baseLodash.cloneDeep(current);
485
+
486
+ changedKeys.forEach((key) => {
487
+ const field = this.fields.find((item) => item.prop === key);
488
+ if (field && typeof field.onChange === "function") {
489
+ field.onChange(current[key], current, field, this);
490
+ }
491
+ // dependsOn:依赖字段变化时清空自身值并刷新远程选项
492
+ this.fields.forEach((item) => {
493
+ if (Array.isArray(item.dependsOn) && item.dependsOn.includes(key)) {
494
+ if (
495
+ !this.$baseLodash.isEqual(
496
+ current[item.prop],
497
+ this.defaultFieldValue(item)
498
+ )
499
+ ) {
500
+ this.$set(this.model, item.prop, this.defaultFieldValue(item));
501
+ }
502
+ this.reloadFieldOptions(item.prop);
503
+ }
504
+ });
505
+ });
506
+ },
507
+ // 供 onChange 回调使用:手动覆盖某字段选项
508
+ setFieldOptions(prop, options) {
509
+ this.$set(this.remoteOptionsMap, prop, options || []);
510
+ },
511
+ // onChange 回调使用:按最新 model 重新拉取词汇/脚本选项
512
+ reloadFieldOptions(prop) {
513
+ const field = this.fields.find((item) => item.prop === prop);
514
+ if (!field) return;
515
+ if (field.dictCode) {
516
+ this.fetchDictOptions(field);
517
+ } else if (field.scriptCode) {
518
+ this.fetchScriptOptions(field);
519
+ }
520
+ },
521
+ loadRemoteOptions() {
522
+ this.remoteOptionsMap = {};
523
+ this.optionsLoadingMap = {};
524
+ this.fields.forEach((field) => {
525
+ if (field.dictCode) {
526
+ this.fetchDictOptions(field);
527
+ } else if (field.scriptCode) {
528
+ this.fetchScriptOptions(field);
529
+ }
530
+ });
531
+ },
532
+ // 词汇:/user/common_attribute/listItems,显示 value、取值 sn(与 xform 约定一致)
533
+ fetchDictOptions(field) {
534
+ this.$set(this.optionsLoadingMap, field.prop, true);
535
+ this.$getBaseDicts({
536
+ code: field.dictCode,
537
+ data:
538
+ typeof field.dictData === "function"
539
+ ? field.dictData(this.model)
540
+ : field.dictData,
541
+ success: ({ dicts }) => {
542
+ this.$set(this.optionsLoadingMap, field.prop, false);
543
+ this.$set(
544
+ this.remoteOptionsMap,
545
+ field.prop,
546
+ dicts.map((item) => ({ label: item.value, value: item.sn }))
547
+ );
548
+ },
549
+ });
550
+ },
551
+ // 脚本编码:"formCode/scriptCode",先取服务名再请求 bd_api 脚本接口
552
+ fetchScriptOptions(field) {
553
+ const parts = String(field.scriptCode)
554
+ .split("/")
555
+ .filter((item) => item);
556
+ if (parts.length < 2) {
557
+ console.warn(
558
+ `[formDialog] scriptCode 需为 "formCode/scriptCode" 格式:${field.scriptCode}`
559
+ );
560
+ return;
561
+ }
562
+ const [formCode, scriptCode] = parts;
563
+ this.$set(this.optionsLoadingMap, field.prop, true);
564
+ this.$http({
565
+ aes: true,
566
+ url: USER_PREFIX + "/formScript/getServiceName",
567
+ method: "post",
568
+ data: { formCode, scriptCode },
569
+ isLoading: false,
570
+ success: (res) => {
571
+ const serviceName = res.objx;
572
+ if (!serviceName) {
573
+ this.$set(this.optionsLoadingMap, field.prop, false);
574
+ this.$baseAlert("服务名不存在");
575
+ return;
576
+ }
577
+ this.$http({
578
+ url: `/${serviceName}/bd_api/${formCode}/${scriptCode}`,
579
+ method: "post",
580
+ data: {
581
+ ...(typeof field.scriptData === "function"
582
+ ? field.scriptData(this.model)
583
+ : field.scriptData),
584
+ },
585
+ isLoading: false,
586
+ success: (res2) => {
587
+ this.$set(this.optionsLoadingMap, field.prop, false);
588
+ let options;
589
+ if (typeof field.parseOptions === "function") {
590
+ options = field.parseOptions(res2) || [];
591
+ } else {
592
+ const objx = res2.objx;
593
+ const rows = Array.isArray(objx)
594
+ ? objx
595
+ : (objx && objx.records) || [];
596
+ const labelField = field.labelField || "label";
597
+ const valueField = field.valueField || "value";
598
+ options = rows.map((row) => ({
599
+ label: row[labelField],
600
+ value: row[valueField],
601
+ row,
602
+ }));
603
+ }
604
+ this.$set(this.remoteOptionsMap, field.prop, options);
605
+ },
606
+ });
607
+ },
608
+ });
609
+ },
610
+ // 搜索框默认禁止手输(值由选择回填),传 props: { editable: true } 可放开
611
+ searchReadonly(field) {
612
+ return !(field.props && field.props.editable === true);
613
+ },
614
+ handleSearch(field) {
615
+ if (this.readonly || this.evalFieldFlag(field.disabled)) return;
616
+ if (typeof field.onSearch === "function") {
617
+ field.onSearch(this.model, field);
618
+ } else if (field.searchComponent) {
619
+ this.openSearchComponent(field);
620
+ } else if (field.searchDialog) {
621
+ this.openXformSearchDialog(field);
622
+ }
623
+ },
624
+ // 方式2:按路径打开自定义 Vue 弹框组件,组件 $emit("confirm", rows) 回写
625
+ openSearchComponent(field) {
626
+ const cfg = field.searchComponent;
627
+ const events = cfg.events || {};
628
+ openComponentDialog(
629
+ {
630
+ wrapper: false,
631
+ ...cfg,
632
+ events: {
633
+ ...events,
634
+ confirm: (...args) => {
635
+ if (typeof events.confirm === "function") {
636
+ events.confirm(...args);
637
+ }
638
+ this.applySearchResult(field, args[0], cfg);
639
+ },
640
+ },
641
+ },
642
+ this
643
+ );
644
+ },
645
+ // 方式3:打开 xform 搜索弹框(searchFormDialog,按 formCode 渲染目标表单的数据表格)
646
+ openXformSearchDialog(field) {
647
+ const cfg = field.searchDialog;
648
+ const { layoutType, fieldMap, onConfirm, showField, ...option } = cfg;
649
+ openComponentDialog(
650
+ {
651
+ componentPath:
652
+ "components/xform/form-designer/form-widget/dialog/searchFormDialog.vue",
653
+ wrapper: false,
654
+ // searchFormDialog 在 xform 环境外运行需要注入 getFormConfig
655
+ provide: {
656
+ getFormConfig: () => ({ layoutType: layoutType || "PC" }),
657
+ },
658
+ props: {
659
+ option: {
660
+ multiple: false,
661
+ ...option,
662
+ rows:
663
+ typeof cfg.rows === "function" ? cfg.rows(this.model) : cfg.rows,
664
+ confirm: (rows, formRef, dialogVm) => {
665
+ if (typeof cfg.confirm === "function") {
666
+ return cfg.confirm(rows, formRef, dialogVm);
667
+ }
668
+ this.applySearchResult(field, rows, cfg);
669
+ },
670
+ },
671
+ },
672
+ },
673
+ this
674
+ );
675
+ },
676
+ // 选中结果回写 model:onConfirm 完全自定义 > fieldMap 逐字段映射 > 默认回写本字段
677
+ applySearchResult(field, rows, cfg) {
678
+ const list = Array.isArray(rows) ? rows : rows ? [rows] : [];
679
+ if (typeof cfg.onConfirm === "function") {
680
+ cfg.onConfirm(this.model, list, field);
681
+ return;
682
+ }
683
+ if (!list.length) return;
684
+ const row = list[0];
685
+ if (cfg.fieldMap) {
686
+ Object.keys(cfg.fieldMap).forEach((modelKey) => {
687
+ const source = cfg.fieldMap[modelKey];
688
+ this.$set(
689
+ this.model,
690
+ modelKey,
691
+ typeof source === "function" ? source(row, list) : row[source]
692
+ );
693
+ });
694
+ } else {
695
+ this.$set(this.model, field.prop, row[cfg.showField || field.prop]);
696
+ }
697
+ },
698
+ handleSearchClear(field) {
699
+ if (this.readonly || this.evalFieldFlag(field.disabled)) return;
700
+ if (typeof field.onClear === "function") {
701
+ field.onClear(this.model, field);
702
+ } else {
703
+ this.model[field.prop] = null;
704
+ }
705
+ },
706
+ controlProps(field) {
707
+ const defaults = {
708
+ placeholder: field.placeholder,
709
+ disabled: this.evalFieldFlag(field.disabled),
710
+ clearable: true,
711
+ };
712
+ switch (field.type) {
713
+ case "textarea":
714
+ Object.assign(defaults, { type: "textarea", rows: 3 });
715
+ break;
716
+ case "search":
717
+ // 搜索框与 xform vabsearch 同款样式:suffix 搜索图标 + clearable;
718
+ // 只读用 v-el-readonly(原生 readonly,不影响清除图标),不走 readonly prop
719
+ defaults.clearable = field.clearable !== false;
720
+ break;
721
+ case "number":
722
+ // baseInputNumber:scale 控制小数位(v-limit-number),negative 放开负号输入
723
+ Object.assign(defaults, {
724
+ scale: field.scale != null ? field.scale : 2,
725
+ negative: field.negative,
726
+ controls: false,
727
+ });
728
+ break;
729
+ case "date":
730
+ Object.assign(defaults, { type: "date", valueFormat: "yyyy-MM-dd" });
731
+ break;
732
+ case "datetime":
733
+ Object.assign(defaults, {
734
+ type: "datetime",
735
+ valueFormat: "yyyy-MM-dd HH:mm:ss",
736
+ });
737
+ break;
738
+ case "daterange":
739
+ Object.assign(defaults, {
740
+ type: "daterange",
741
+ valueFormat: "yyyy-MM-dd",
742
+ });
743
+ break;
744
+ }
745
+ const merged = { ...defaults, ...field.props };
746
+ if (field.type === "search") {
747
+ // editable 只用于 searchReadonly 判断,不透传给 el-input
748
+ delete merged.editable;
749
+ }
750
+ return merged;
751
+ },
752
+ settle(value) {
753
+ if (this._settled) return;
754
+ this._settled = true;
755
+ if (this._resolve) {
756
+ this._resolve(value);
757
+ }
758
+ },
759
+ handleConfirm() {
760
+ if (this.xformMode) {
761
+ this.handleXformConfirm();
762
+ return;
763
+ }
764
+ this.$refs.form.validate((valid) => {
765
+ if (!valid) return;
766
+ this.confirmWithData({ ...this.model });
767
+ });
768
+ },
769
+ confirmWithData(data) {
770
+ if (typeof this._beforeConfirm === "function") {
771
+ this.confirmLoading = true;
772
+ Promise.resolve()
773
+ .then(() => this._beforeConfirm(data))
774
+ .then(() => {
775
+ this.confirmLoading = false;
776
+ this.settle(data);
777
+ this.showWrap = false;
778
+ })
779
+ .catch(() => {
780
+ // beforeConfirm 失败时不关闭弹框,由调用方自行提示错误
781
+ this.confirmLoading = false;
782
+ });
783
+ } else {
784
+ this.settle(data);
785
+ this.showWrap = false;
786
+ }
787
+ },
788
+ handleClose() {
789
+ // 取消/右上角关闭:resolve(null);确认路径已先 settle,此处不生效
790
+ this.settle(null);
791
+ },
792
+ handleClosed() {
793
+ this.$destroy();
794
+ if (this.$el && this.$el.parentNode) {
795
+ this.$el.parentNode.removeChild(this.$el);
796
+ }
797
+ },
798
+ },
799
+ };
800
+ </script>
801
+
802
+ <style></style>