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

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 (158) hide show
  1. package/package.json +2 -1
  2. package/src/components/VabUpload/index.vue +1 -1
  3. package/src/components/baseInputExport/mixins.js +13 -2
  4. package/src/components/code-editor/async.js +29 -0
  5. package/src/components/code-editor/index.vue +65 -6
  6. package/src/components/excelImport/index.js +69 -7
  7. package/src/components/excelImport/mixins.js +841 -9
  8. package/src/components/fileLibrary/fileHistoryDialog.vue +1 -1
  9. package/src/components/fileLibrary/fileObjAuthEdit.vue +1 -1
  10. package/src/components/fileLibrary/filterDialog.vue +1 -1
  11. package/src/components/formDialog/README.md +324 -241
  12. package/src/components/formDialog/actionButtons.js +90 -0
  13. package/src/components/formDialog/index.js +48 -35
  14. package/src/components/formDialog/index.vue +802 -724
  15. package/src/components/formDrawer/drawer.vue +719 -0
  16. package/src/components/formDrawer/index.js +50 -0
  17. package/src/components/onlineTalk/talkUserDialog.vue +1 -1
  18. package/src/components/table/index.js +4 -0
  19. package/src/components/tempStorage/tempStorageDialog.vue +1 -1
  20. package/src/components/wf/addTaskUserdialog.vue +1 -1
  21. package/src/components/xform/docs/2026-08 /345/210/227/350/241/250/345/257/274/345/207/272/350/204/261/346/225/217/345/210/227/345/211/224/351/231/244/350/220/275/345/234/260/346/226/207/346/241/243.md" +165 -0
  22. package/src/components/xform/form-designer/form-widget/container-widget/data-table-widget.vue +1 -1
  23. package/src/components/xform/form-designer/form-widget/dialog/formDialog.vue +115 -4
  24. package/src/components/xform/form-designer/form-widget/dialog/formDrawer.vue +102 -5
  25. package/src/components/xform/form-designer/form-widget/dialog/formFieldDialog.vue +1 -1
  26. package/src/components/xform/form-designer/form-widget/dialog/vabSearchDialog.vue +1 -1
  27. package/src/components/xform/form-designer/form-widget/field-widget/form-item-wrapper.vue +18 -73
  28. package/src/components/xform/form-designer/form-widget/field-widget/script-input-widget.vue +3 -0
  29. package/src/components/xform/form-designer/setting-panel/action-button-config-dialog.vue +241 -0
  30. package/src/components/xform/form-designer/setting-panel/form-setting.vue +116 -0
  31. package/src/components/xform/form-designer/setting-panel/property-editor/container-detail/detail-editor.vue +6 -0
  32. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +1 -0
  33. package/src/components/xform/form-render/container-item/data-table-mixin.js +1480 -1460
  34. package/src/components/xform/form-render/container-item/detail-item.vue +11 -2
  35. package/src/components/xform/form-render/container-item/list-h5-item.vue +12 -0
  36. package/src/components/xform/form-render/indexMixin.js +5 -0
  37. package/src/components/xform/utils/textMaskUtil.js +186 -0
  38. package/src/index.js +505 -472
  39. package/src/store/config/index.js +224 -0
  40. package/src/store/modules/tagsView.js +4 -2
  41. package/src/utils/lazyPlugin.js +66 -0
  42. package/src/views/bd/project/branch/bd_branch/edit.vue +145 -0
  43. package/src/views/bd/project/branch/bd_branch/list.vue +72 -0
  44. package/src/views/bd/project/branch/bd_branch/menuKindDialog.vue +106 -0
  45. package/src/views/bd/project/branch/bd_branch/mixins/edit.js +189 -0
  46. package/src/views/bd/project/branch/bd_branch/mixins/list.js +124 -0
  47. package/src/views/bd/project/branch/bd_branch/mixins/menuKindDialog.js +85 -0
  48. package/src/views/bd/project/branch/form_script/list.vue +22 -0
  49. package/src/views/bd/project/branch/form_template/list.vue +21 -0
  50. package/src/views/bd/project/branch/menu_kind/list.vue +21 -0
  51. package/src/views/bd/project/common/form_script/list.vue +16 -0
  52. package/src/views/bd/project/common/form_template/list.vue +15 -0
  53. package/src/views/bd/project/common/menu_kind/list.vue +15 -0
  54. package/src/views/bd/project/core/branchContext.js +100 -0
  55. package/src/views/bd/project/core/branchPage.js +78 -0
  56. package/src/views/bd/project/core/branchPageWrap.vue +216 -0
  57. package/src/views/bd/project/core/branchScope.js +44 -0
  58. package/src/views/bd/project/core/branchSelect.vue +106 -0
  59. package/src/views/bd/project/core/httpBranchPatch.js +58 -0
  60. package/src/views/bd/project/datatable/menu_kind/list.vue +15 -0
  61. package/src/views/bd/project/datatable/table_model/list.vue +15 -0
  62. package/src/views/bd/setting/bd_attach_setting/dialog.vue +1 -1
  63. package/src/views/bd/setting/bd_attach_setting/list.vue +1 -1
  64. package/src/views/bd/setting/bd_company_env/dialog.vue +1 -1
  65. package/src/views/bd/setting/bd_company_env/list.vue +1 -1
  66. package/src/views/bd/setting/formVersion/ftHistoryDialog.vue +1 -1
  67. package/src/views/bd/setting/form_import_log/list.vue +1 -1
  68. package/src/views/bd/setting/form_script/dialog.vue +1 -1
  69. package/src/views/bd/setting/form_script/form_list.vue +1 -1
  70. package/src/views/bd/setting/form_script/list.vue +1 -1
  71. package/src/views/bd/setting/form_script/list1.vue +1 -1
  72. package/src/views/bd/setting/form_template/dialog.vue +1 -1
  73. package/src/views/bd/setting/form_template/formDesignerDialog.vue +1 -0
  74. package/src/views/bd/setting/form_template/ftHistoryDialog.vue +1 -1
  75. package/src/views/bd/setting/form_template/itemList.vue +1 -1
  76. package/src/views/bd/setting/form_template/list.vue +3 -3
  77. package/src/views/bd/setting/form_template/mixins/edit.js +3 -3
  78. package/src/views/bd/setting/form_template/mixins/list.js +3 -3
  79. package/src/views/bd/setting/form_template/mixins/list2.js +3 -3
  80. package/src/views/bd/setting/form_template/mixins/wf_list.js +3 -3
  81. package/src/views/bd/setting/form_template/wf_list.vue +3 -3
  82. package/src/views/bd/setting/logic_param/list.vue +1 -1
  83. package/src/views/bd/setting/menu_kind/dialog.vue +1 -1
  84. package/src/views/bd/setting/menu_kind/list.vue +1 -1
  85. package/src/views/bd/setting/push_data/list.vue +1 -1
  86. package/src/views/bd/setting/push_data_h/list.vue +1 -1
  87. package/src/views/bd/setting/request_async_setting/list.vue +1 -1
  88. package/src/views/bd/setting/request_setting/list.vue +1 -1
  89. package/src/views/bd/setting/table_model/dialog.vue +1 -1
  90. package/src/views/bd/setting/table_model/list.vue +1 -1
  91. package/src/views/bd/setting/table_model/zdDialog.vue +1 -1
  92. package/src/views/support/export_template/list.vue +1 -1
  93. package/src/views/user/access_log/list.vue +1 -1
  94. package/src/views/user/access_log/list2.vue +1 -1
  95. package/src/views/user/api_request/list.vue +1 -1
  96. package/src/views/user/area/dialog.vue +1 -1
  97. package/src/views/user/area/list.vue +1 -1
  98. package/src/views/user/bill_setting/list.vue +1 -1
  99. package/src/views/user/bill_setting/userDialog.vue +1 -1
  100. package/src/views/user/code_rules/list.vue +1 -1
  101. package/src/views/user/common_attribute/list.vue +1 -1
  102. package/src/views/user/common_script/list.vue +1 -1
  103. package/src/views/user/company_info/dialog.vue +1 -1
  104. package/src/views/user/country/dialog.vue +1 -1
  105. package/src/views/user/country/list.vue +1 -1
  106. package/src/views/user/data_type_setting/dialog.vue +1 -1
  107. package/src/views/user/extend_datasource/dialog.vue +1 -1
  108. package/src/views/user/extend_datasource/list.vue +1 -1
  109. package/src/views/user/fieldTranslation/list.vue +1 -1
  110. package/src/views/user/field_values_invisible/list.vue +1 -1
  111. package/src/views/user/file_type/list.vue +1 -1
  112. package/src/views/user/file_view_area/list.vue +1 -1
  113. package/src/views/user/form/form_template/dialog.vue +1 -1
  114. package/src/views/user/form/form_template/itemList.vue +1 -1
  115. package/src/views/user/form/form_template/list.vue +1 -1
  116. package/src/views/user/form/form_template_field/dialog.vue +1 -1
  117. package/src/views/user/form/form_type/dialog.vue +1 -1
  118. package/src/views/user/form/form_type/list.vue +1 -1
  119. package/src/views/user/form/report_requestaccess/dialog.vue +1 -1
  120. package/src/views/user/form/report_requestaccess/list.vue +1 -1
  121. package/src/views/user/form/vform/designer.vue +51 -22
  122. package/src/views/user/groups/dialog.vue +1 -1
  123. package/src/views/user/groups/list.vue +1 -1
  124. package/src/views/user/lang_tag/dialog.vue +1 -1
  125. package/src/views/user/lang_tag/list.vue +1 -1
  126. package/src/views/user/language_setting/list.vue +1 -1
  127. package/src/views/user/menu/list.vue +1 -1
  128. package/src/views/user/mobile_menu/list.vue +1 -1
  129. package/src/views/user/notify_message/list.vue +1 -1
  130. package/src/views/user/notify_template/list.vue +1 -1
  131. package/src/views/user/notify_template/list2.vue +1 -1
  132. package/src/views/user/oplog/list.vue +1 -1
  133. package/src/views/user/position/dialog.vue +1 -1
  134. package/src/views/user/position/list.vue +1 -1
  135. package/src/views/user/project_tag/dialog.vue +1 -1
  136. package/src/views/user/project_tag/list.vue +1 -1
  137. package/src/views/user/push_setting/list.vue +1 -1
  138. package/src/views/user/request_setting/list.vue +1 -1
  139. package/src/views/user/role/dialog.vue +1 -1
  140. package/src/views/user/role/list.vue +1 -1
  141. package/src/views/user/sale_org/dialog.vue +1 -1
  142. package/src/views/user/sale_org/list.vue +1 -1
  143. package/src/views/user/system_notice/list.vue +1 -1
  144. package/src/views/user/system_parameter/list.vue +1 -1
  145. package/src/views/user/user/dialog.vue +1 -1
  146. package/src/views/user/user/form_dialog.vue +1 -1
  147. package/src/views/user/user/form_list.vue +1 -1
  148. package/src/views/user/user/list.vue +2 -2
  149. package/src/views/user/user_log_classify/list.vue +1 -1
  150. package/src/views/user/wf/wfReport/index.vue +2 -2
  151. package/src/views/user/wf/wf_auto_submit_data/list.vue +2 -2
  152. package/src/views/user/wf/wf_manage/list.vue +2 -2
  153. package/src/views/user/wf/wf_manage/list2.vue +2 -2
  154. package/src/views/user/wf/wf_obj_config/dialog.vue +1 -1
  155. package/src/views/user/wf/wf_obj_config/list.vue +1 -1
  156. package/src/views/user/wf/wf_obj_config/list_form.vue +1 -1
  157. package/src/views/user/wf/wf_transfer_setting/list.vue +1 -1
  158. 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>