cloud-web-corejs 1.0.54-dev.709 → 1.0.54-dev.710

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cloud-web-corejs",
3
3
  "private": false,
4
- "version": "1.0.54-dev.709",
4
+ "version": "1.0.54-dev.710",
5
5
  "scripts": {
6
6
  "dev": "vue-cli-service serve",
7
7
  "lint": "eslint --ext .js,.vue src",
@@ -0,0 +1,128 @@
1
+ # autocomplete 自动完成组件
2
+
3
+ 基于 `el-autocomplete` 的远程搜索表单字段,支持**同时存储 ID 和名称**,可配置**后台逻辑脚本编码**进行远程搜索。
4
+
5
+ ## 特性
6
+
7
+ - 远程搜索:输入关键词实时调用后台逻辑脚本获取候选项
8
+ - 双值存储:选中后 ID 存入字段本身(`keyName`),名称存入展示字段(`vabSearchName`)
9
+ - 逻辑脚本配置:复用 `httpConfig`(`formScriptCode` / `formScriptParam` / `formScriptSuccess`)
10
+ - 选中回填:选中某项后可自动回填其他表单字段
11
+ - ID 反查:编辑态可选通过反查脚本根据 ID 获取名称回显
12
+ - 自定义下拉项模板:支持脚本自定义候选项展示 HTML
13
+ - 支持子表单、只读模式、流程属性、显示规则等通用能力
14
+
15
+ ## 文件清单
16
+
17
+ | 文件 | 作用 |
18
+ |------|------|
19
+ | `form-designer/form-widget/field-widget/autocomplete-widget.vue` | 运行期/设计期共用字段组件 |
20
+ | `form-designer/form-widget/field-widget/mixins/autocomplete-mixin.js` | 核心逻辑(远程搜索、选择、回填、反查) |
21
+ | `form-designer/widget-panel/widgetsConfig.js` | `autocompleteConfig` schema + 注册到 `advancedFields` |
22
+ | `form-designer/setting-panel/property-editor/field-autocomplete/autocomplete-vabSearchName-editor.vue` | 属性编辑器(展示字段、回填配置等) |
23
+ | `lang/zh-CN.js` / `lang/en-US.js` | 组件库标签 i18n |
24
+
25
+ ## 设计器配置
26
+
27
+ 在表单设计器左侧「高级字段」分组中拖入「自动完成」组件,右侧属性面板可配置:
28
+
29
+ ### 自动完成设置(`autocomplete-vabSearchName-editor`)
30
+ - **存储ID字段**(`valueField`):候选项数据中 ID 对应的字段名,留空则使用字段本身 `keyName`
31
+ - **存储名称字段**(`vabSearchName`):选中后名称持久化到的表单字段名
32
+ - **输入提示**(`placeholder`)
33
+ - **可清空**(`clearable`)
34
+ - **聚焦即查询**(`triggerOnFocus`):聚焦空输入框时是否触发一次查询
35
+ - **防抖(ms)**(`debounce`):输入防抖毫秒数
36
+ - **ID反查脚本编码**(`getByIdScriptCode`):选填,编辑态根据 ID 反查名称
37
+ - **清空回调**(`onSearchClear`)
38
+ - **下拉项模板**(`autocompleteItemTemplate`):脚本,入参 `item`,返回 HTML 字符串
39
+ - **选中回填**(`autocompleteFillConfig`):数组,每项 `{ targetField, targetFormField, sourceField }`
40
+
41
+ ### 请求访问设置(复用 `formScriptEnabled-editor`)
42
+ - **表单脚本编码**(`formScriptCode`):远程搜索的后台逻辑脚本编码(必填)
43
+ - **查询参数**(`formScriptParam`):脚本,返回额外请求参数对象
44
+ - **查询回调**(`formScriptSuccess`):脚本,入参 `res`、`rows`,可返回处理后的数组
45
+
46
+ ## 远程搜索请求约定
47
+
48
+ `querySearchAsync` 调用 `formHttp`,请求结构:
49
+
50
+ ```js
51
+ {
52
+ scriptCode: formScriptCode, // 配置的逻辑脚本编码
53
+ data: {
54
+ formCode, // 当前表单编码
55
+ formVersion, // 表单版本
56
+ taBm: fieldKeyName, // 字段名
57
+ data: {
58
+ ...accessParam, // formScriptParam 返回的额外参数
59
+ keyword: queryString // 用户输入的关键词
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ 后台脚本返回的数据(`res.objx`)应为数组(或 `{ records: [...] }`),每个元素至少包含:
66
+ - ID 字段:对应配置的 `valueField`(或字段 `keyName`)
67
+ - 名称字段:对应配置的 `vabSearchName`
68
+
69
+ `formScriptSuccess` 回调可对返回结果二次处理,返回新数组。
70
+
71
+ ## 选中行为
72
+
73
+ `handleSelect(item)`:
74
+ 1. 取 `item[valueField]` 作为 ID,写入 `currentData[fieldKeyName]`(提交字段)
75
+ 2. 取 `item[labelField]` 作为名称,写入 `currentData[vabSearchName]`(展示字段)
76
+ 3. 按 `autocompleteFillConfig` 把 `item[sourceField]` 回填到目标字段/属性
77
+ 4. 触发 `onChange(value, oldValue, item)` 自定义事件
78
+
79
+ ## 清空行为
80
+
81
+ `handleClear()`:同时清空 ID 与名称,触发 `onSearchClear`。
82
+
83
+ ## 编辑态回显
84
+
85
+ - 默认:表单数据中 ID(`keyName`)与名称(`vabSearchName`)同时存在时,直接回显名称
86
+ - 仅 ID 无名称:若配置了 `getByIdScriptCode`,`mounted` 时自动调用反查脚本获取名称回显
87
+
88
+ 反查脚本请求结构:
89
+ ```js
90
+ {
91
+ scriptCode: getByIdScriptCode,
92
+ data: { formCode, formVersion, taBm, data: { ...accessParam, id } }
93
+ }
94
+ ```
95
+
96
+ ## 与 vabsearch 的区别
97
+
98
+ | 维度 | vabsearch | autocomplete |
99
+ |------|-----------|--------------|
100
+ | 交互 | 点击弹框选择 | 输入实时搜索下拉选择 |
101
+ | 组件 | `el-input` + 弹框 | `el-autocomplete` |
102
+ | 多选 | 支持 | 不支持(单选) |
103
+ | 脚本 | 弹框表单编码 | 逻辑脚本编码(远程搜索) |
104
+ | 存储 | ID + 名称 | ID + 名称(同) |
105
+
106
+ ## 关键选项 JSON 示例
107
+
108
+ ```json
109
+ {
110
+ "type": "autocomplete",
111
+ "options": {
112
+ "name": "input123456",
113
+ "keyNameEnabled": true,
114
+ "keyName": "customer_id",
115
+ "vabSearchName": "customer_name",
116
+ "label": "客户",
117
+ "valueField": "id",
118
+ "formScriptEnabled": true,
119
+ "formScriptCode": "searchCustomer",
120
+ "triggerOnFocus": true,
121
+ "debounce": 300,
122
+ "clearable": true,
123
+ "autocompleteFillConfig": [
124
+ { "targetFormField": "customer_code", "sourceField": "code" }
125
+ ]
126
+ }
127
+ }
128
+ ```
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <form-item-wrapper :designer="designer" :field="field" :rules="rules" :design-state="designState"
3
+ :parent-widget="parentWidget" :parent-list="parentList" :index-of-parent-list="indexOfParentList"
4
+ :sub-form-row-index="subFormRowIndex" :sub-form-col-index="subFormColIndex"
5
+ :sub-form-row-id="subFormRowId">
6
+ <el-autocomplete
7
+ ref="fieldEditor"
8
+ v-model="displayValue"
9
+ v-show="!isReadMode"
10
+ class="full-width-input"
11
+ :disabled="field.options.disabled"
12
+ :readonly="field.options.readonly"
13
+ :size="field.options.size"
14
+ :placeholder="getI18nLabel(field.options.placeholder || '请输入关键词搜索')"
15
+ :clearable="field.options.clearable"
16
+ :fetch-suggestions="querySearchAsync"
17
+ :trigger-on-focus="field.options.triggerOnFocus !== false"
18
+ :debounce="field.options.debounce || 300"
19
+ :value-key="valueKey"
20
+ :prefix-icon="field.options.prefixIcon"
21
+ :suffix-icon="field.options.suffixIcon"
22
+ :popper-class="field.options.customClass ? field.options.customClass.join(' ') : ''"
23
+ @select="handleSelect"
24
+ @clear="handleClear"
25
+ @focus="handleFocusCustomEvent"
26
+ @blur="handleBlurCustomEvent"
27
+ @input="handleInputChange"
28
+ :style="'width:' + field.options.widgetWidth + ' !important;'"
29
+ :class="{'custom-width': field.options.widgetWidth != null}">
30
+ <template slot-scope="{ item }">
31
+ <template v-if="field.options.autocompleteItemTemplate">
32
+ <!-- 支持自定义下拉项模板脚本(返回 HTML 字符串) -->
33
+ <span v-html="renderItemTemplate(item)"></span>
34
+ </template>
35
+ <template v-else>
36
+ <span>{{ item[valueKey] }}</span>
37
+ </template>
38
+ </template>
39
+ </el-autocomplete>
40
+ <template v-if="isReadMode">
41
+ <span class="readonly-mode-field">{{ displayValue }}</span>
42
+ </template>
43
+ </form-item-wrapper>
44
+ </template>
45
+
46
+ <script>
47
+ import mixins from "@base/components/xform/form-designer/form-widget/field-widget/mixins/autocomplete-mixin";
48
+
49
+ export default {
50
+ name: "autocomplete-widget",
51
+ componentName: 'FieldWidget', //必须固定为FieldWidget,用于接收父级组件的broadcast事件
52
+ mixins: [mixins],
53
+ methods: {
54
+ /**
55
+ * 渲染自定义下拉项模板(可选)。
56
+ * autocompleteItemTemplate 为一段返回 HTML 字符串的脚本,入参为 item
57
+ */
58
+ renderItemTemplate(item) {
59
+ if (!this.field.options.autocompleteItemTemplate) return "";
60
+ try {
61
+ let fn = new Function("item", this.field.options.autocompleteItemTemplate);
62
+ return fn.call(this, item) || "";
63
+ } catch (e) {
64
+ return "";
65
+ }
66
+ },
67
+ },
68
+ }
69
+ </script>
70
+
71
+ <style lang="scss" scoped>
72
+ @import "~@/styles/global.scss";
73
+
74
+ .full-width-input {
75
+ width: 100% !important;
76
+ }
77
+ </style>
@@ -0,0 +1,377 @@
1
+ import FormItemWrapper from "@base/components/xform/form-designer/form-widget/field-widget/form-item-wrapper";
2
+ import emitter from "@base/components/xform/utils/emitter";
3
+ import i18n from "@base/components/xform/utils/i18n";
4
+ import { deepClone, trim } from "@base/components/xform/utils/util";
5
+ import fieldMixin from "@base/components/xform/form-designer/form-widget/field-widget/fieldMixin";
6
+
7
+ export default {
8
+ mixins: [emitter, fieldMixin, i18n],
9
+ props: {
10
+ field: Object,
11
+ parentWidget: Object,
12
+ parentList: Array,
13
+ indexOfParentList: Number,
14
+ designer: Object,
15
+
16
+ designState: {
17
+ type: Boolean,
18
+ default: false,
19
+ },
20
+
21
+ columnConfig: {
22
+ type: Object,
23
+ default: null,
24
+ },
25
+ subFormRowIndex: {
26
+ /* 子表单组件行索引,从0开始计数 */
27
+ type: Number,
28
+ default: -1,
29
+ },
30
+ subFormColIndex: {
31
+ /* 子表单组件列索引,从0开始计数 */
32
+ type: Number,
33
+ default: -1,
34
+ },
35
+ subFormRowId: {
36
+ /* 子表单组件行Id,唯一id且不可变 */
37
+ type: String,
38
+ default: "",
39
+ },
40
+ },
41
+ components: {
42
+ FormItemWrapper,
43
+ },
44
+ data() {
45
+ return {
46
+ oldFieldValue: null, //field组件change之前的值
47
+ fieldModel: null, //存储 ID 值(提交字段)
48
+ displayValue: null, //el-autocomplete 的 v-model,存储展示名称(仅用于输入框显示)
49
+ rules: [],
50
+ isSelecting: false, //标记当前是否由"选择某一项"触发的输入变化
51
+ lastQueryKeyword: null, //最近一次查询关键词,避免重复查询
52
+ };
53
+ },
54
+ watch: {
55
+ fieldModel(val) {
56
+ //同步 ID 到当前数据行/表单模型
57
+ let currentData = this.currentData;
58
+ let fieldKeyName = this.fieldKeyName;
59
+ currentData[fieldKeyName] = val;
60
+ },
61
+ showValue(val) {
62
+ //外部回显:当 currentData[labelField] 变化时,同步到输入框
63
+ this.displayValue = val;
64
+ },
65
+ },
66
+ computed: {
67
+ /**
68
+ * 选项数据中"ID/值"对应的字段名
69
+ */
70
+ valueField() {
71
+ return trim(this.field.options.valueField) || this.fieldKeyName;
72
+ },
73
+ /**
74
+ * 选项数据中"名称"对应的字段名(同时作为持久化到表单数据的展示字段)
75
+ */
76
+ labelField() {
77
+ return trim(this.field.options.vabSearchName) || this.fieldKeyName;
78
+ },
79
+ /**
80
+ * el-autocomplete 下拉项展示用的 value-key
81
+ */
82
+ valueKey() {
83
+ return this.labelField;
84
+ },
85
+ tableRow() {
86
+ let tableParam = this.tableParam;
87
+ return tableParam && tableParam.row ? tableParam.row : null;
88
+ },
89
+ currentData() {
90
+ let tableParam = this.tableParam;
91
+ return tableParam && tableParam.row ? tableParam.row : this.formModel;
92
+ },
93
+ /**
94
+ * 表单数据中持久化的展示名称,用于回显
95
+ */
96
+ showValue() {
97
+ let currentData = this.currentData;
98
+ let labelField = this.labelField;
99
+ let value = currentData[labelField] ?? null;
100
+ this.displayValue = value;
101
+ return value;
102
+ },
103
+ },
104
+ beforeCreate() {
105
+ /* 这里不能访问方法和属性!! */
106
+ },
107
+
108
+ created() {
109
+ /* 注意:子组件mounted在父组件created之后、父组件mounted之前触发,故子组件mounted需要用到的prop
110
+ 需要在父组件created中初始化!! */
111
+ this.initFieldModel();
112
+ this.registerToRefList();
113
+ this.initEventHandler();
114
+ this.buildFieldRules();
115
+
116
+ this.handleOnCreated();
117
+ },
118
+
119
+ mounted() {
120
+ this.handleOnMounted();
121
+ //编辑态回显:若有 ID 但无名称,且配置了反查脚本,则自动反查
122
+ this.$nextTick(() => {
123
+ if (this.designState) return;
124
+ let id = this.fieldModel;
125
+ let label = this.currentData[this.labelField];
126
+ if (this.isNotNullVal(id) && !this.isNotNullVal(label)) {
127
+ this.fetchLabelById(id);
128
+ }
129
+ });
130
+ },
131
+
132
+ beforeDestroy() {
133
+ this.unregisterFromRefList();
134
+ },
135
+
136
+ methods: {
137
+ /**
138
+ * 设置展示名称到当前数据行/表单模型,并同步到输入框
139
+ */
140
+ setShowValue(val = null) {
141
+ let labelField = this.labelField;
142
+ this.currentData[labelField] = val;
143
+ this.displayValue = val;
144
+ },
145
+ setValue: function (e) {
146
+ if (this.field.formItemFlag) {
147
+ let value = e ?? null;
148
+ let currentData = this.currentData;
149
+ if (void 0 === currentData[this.fieldKeyName]) {
150
+ this.$set(currentData, this.fieldKeyName, null);
151
+ }
152
+ let t = deepClone(this.fieldModel);
153
+ this.fieldModel = value;
154
+ this.initFileList();
155
+ this.syncUpdateFormModel(value);
156
+ this.emitFieldDataChange(value, t);
157
+ }
158
+ },
159
+ fieldModelLabel() {
160
+ return this.displayValue;
161
+ },
162
+ /**
163
+ * 远程搜索:调用后台逻辑脚本,返回建议列表
164
+ * el-autocomplete 的 fetch-suggestions 回调签名 (queryString, callback)
165
+ */
166
+ querySearchAsync(queryString, cb) {
167
+ if (this.designState) {
168
+ cb([]);
169
+ return;
170
+ }
171
+ let formScriptEnabled = this.field.options.formScriptEnabled || false;
172
+ let scriptCode = this.field.options.formScriptCode;
173
+ if (!formScriptEnabled || !scriptCode) {
174
+ cb([]);
175
+ return;
176
+ }
177
+ //避免对相同关键词重复查询(el-autocomplete focus/input 都会触发)
178
+ let keyword = queryString == null ? "" : String(queryString);
179
+ if (
180
+ keyword === this.lastQueryKeyword
181
+ && this._lastSuggestions
182
+ && this._lastSuggestions.length
183
+ ) {
184
+ cb(this._lastSuggestions);
185
+ return;
186
+ }
187
+ this.lastQueryKeyword = keyword;
188
+
189
+ let reportTemplate = this.getFormRef().reportTemplate;
190
+ let formCode = reportTemplate.formCode;
191
+ let accessParam = this.handleCustomEvent(
192
+ this.field.options.formScriptParam
193
+ );
194
+ let taBm = this.fieldKeyName;
195
+ let data = {
196
+ formCode: formCode,
197
+ formVersion: reportTemplate.formVersion,
198
+ taBm: taBm,
199
+ data: {
200
+ ...accessParam,
201
+ keyword: keyword,
202
+ },
203
+ };
204
+ return this.formHttp({
205
+ scriptCode: scriptCode,
206
+ isLoading: false,
207
+ data: data,
208
+ callback: (res) => {
209
+ let rows = res.objx?.records || res.objx || [];
210
+ //允许通过 formScriptSuccess 对结果进行二次处理
211
+ let handled = this.handleCustomEvent(
212
+ this.field.options.formScriptSuccess,
213
+ ["res", "rows"],
214
+ [res, rows]
215
+ );
216
+ if (Array.isArray(handled)) {
217
+ rows = handled;
218
+ }
219
+ this._lastSuggestions = rows;
220
+ cb(rows);
221
+ },
222
+ fail: () => {
223
+ cb([]);
224
+ },
225
+ error: () => {
226
+ cb([]);
227
+ },
228
+ });
229
+ },
230
+ /**
231
+ * 选中某一项:存储 ID 与名称,并回填配置的其他字段
232
+ */
233
+ handleSelect(item) {
234
+ this.isSelecting = true;
235
+ try {
236
+ let valueField = this.valueField;
237
+ let labelField = this.labelField;
238
+ let id = item[valueField] ?? null;
239
+ let label = item[labelField] ?? null;
240
+
241
+ //存储 ID(提交字段)
242
+ this.setValue(id);
243
+ //存储展示名称(持久化到表单数据 + 输入框)
244
+ this.setShowValue(label);
245
+
246
+ //回填其他字段
247
+ this.fillBackFields(item);
248
+
249
+ //触发 onChange 自定义事件
250
+ if (this.field.options.onChange) {
251
+ let fn = new Function(
252
+ "value",
253
+ "oldValue",
254
+ "item",
255
+ this.field.options.onChange
256
+ );
257
+ fn.call(this, id, this.oldFieldValue, item);
258
+ }
259
+ } finally {
260
+ this.$nextTick(() => {
261
+ this.isSelecting = false;
262
+ });
263
+ }
264
+ },
265
+ /**
266
+ * 清空:同时清除 ID 与名称
267
+ */
268
+ handleClear() {
269
+ this.setValue(null);
270
+ this.setShowValue(null);
271
+ this.lastQueryKeyword = null;
272
+ this._lastSuggestions = null;
273
+ if (this.field.options.onSearchClear) {
274
+ let tableParam = this.tableParam;
275
+ let eventParamNames = [];
276
+ let eventParamValues = [];
277
+ if (tableParam) {
278
+ eventParamNames = ["rowData", "rowIndex"];
279
+ eventParamValues = [tableParam.row, tableParam.rowIndex];
280
+ }
281
+ this.handleCustomEvent(
282
+ this.field.options.onSearchClear,
283
+ eventParamNames,
284
+ eventParamValues
285
+ );
286
+ }
287
+ },
288
+ /**
289
+ * 输入框内容变化:若非"选中"触发,且与已存储名称不一致,则清空 ID(用户正在重新搜索)
290
+ */
291
+ handleInputChange(text) {
292
+ if (this.isSelecting) return;
293
+ if (!this.designState) {
294
+ if (!text) {
295
+ //输入框被清空(非通过 clear 按钮)
296
+ this.setValue(null);
297
+ this.setShowValue(null);
298
+ return;
299
+ }
300
+ let storedLabel = this.currentData[this.labelField];
301
+ if (text !== storedLabel) {
302
+ //用户手动输入了与已选中名称不同的内容,清空已存储的 ID 与名称
303
+ this.fieldModel = null;
304
+ this.currentData[this.fieldKeyName] = null;
305
+ this.currentData[this.labelField] = null;
306
+ this.oldFieldValue = null;
307
+ }
308
+ }
309
+ //同步到表单模型并派发校验
310
+ this.dispatch("VFormRender", "fieldValidation", [this.getPropName()]);
311
+ if (this.field.options.onInput) {
312
+ let fn = new Function("value", this.field.options.onInput);
313
+ fn.call(this, text);
314
+ }
315
+ },
316
+ /**
317
+ * 根据回填配置把选中项的其他字段值写入表单
318
+ */
319
+ fillBackFields(item) {
320
+ let fillConfig = this.field.options.autocompleteFillConfig || [];
321
+ if (!fillConfig.length) return;
322
+ let formModel = this.formModel;
323
+ fillConfig.forEach((cfg) => {
324
+ let targetField = trim(cfg.targetField);
325
+ let targetFormField = trim(cfg.targetFormField);
326
+ let sourceField = trim(cfg.sourceField);
327
+ let value = sourceField ? item[sourceField] ?? null : null;
328
+ if (targetField) {
329
+ let ref = this.getWidgetRef(targetField);
330
+ if (ref && ref.setValue) {
331
+ ref.setValue(value);
332
+ }
333
+ } else if (targetFormField) {
334
+ formModel[targetFormField] = value;
335
+ }
336
+ });
337
+ },
338
+ /**
339
+ * 通过 ID 反查名称(编辑态回显场景)。
340
+ * 当 fieldModel 有值但 currentData[labelField] 为空时调用。
341
+ */
342
+ fetchLabelById(id) {
343
+ let formScriptEnabled = this.field.options.formScriptEnabled || false;
344
+ let scriptCode = this.field.options.formScriptCode;
345
+ let getByIdScriptCode = this.field.options.getByIdScriptCode;
346
+ if (!formScriptEnabled || !id) return;
347
+ //优先使用专用的 getById 脚本;否则不自动反查
348
+ if (!getByIdScriptCode) return;
349
+ let reportTemplate = this.getFormRef().reportTemplate;
350
+ let formCode = reportTemplate.formCode;
351
+ let accessParam = this.handleCustomEvent(
352
+ this.field.options.formScriptParam
353
+ );
354
+ return this.formHttp({
355
+ scriptCode: getByIdScriptCode,
356
+ isLoading: false,
357
+ data: {
358
+ formCode: formCode,
359
+ formVersion: reportTemplate.formVersion,
360
+ taBm: this.fieldKeyName,
361
+ data: {
362
+ ...accessParam,
363
+ id: id,
364
+ },
365
+ },
366
+ callback: (res) => {
367
+ let rows = res.objx?.records || res.objx || [];
368
+ let row = Array.isArray(rows) ? rows[0] : rows;
369
+ if (row) {
370
+ let label = row[this.labelField] ?? null;
371
+ this.setShowValue(label);
372
+ }
373
+ },
374
+ });
375
+ },
376
+ },
377
+ };
@@ -0,0 +1,201 @@
1
+ <template>
2
+ <div>
3
+ <el-form-item label-width="0">
4
+ <el-divider class="custom-divider-margin-top">自动完成设置</el-divider>
5
+ </el-form-item>
6
+
7
+ <el-form-item label="存储ID字段">
8
+ <el-input
9
+ type="text"
10
+ v-model="optionModel.valueField"
11
+ placeholder="留空则使用字段本身"
12
+ ></el-input>
13
+ </el-form-item>
14
+ <el-form-item label="存储名称字段">
15
+ <el-input
16
+ type="text"
17
+ v-model="optionModel.vabSearchName"
18
+ placeholder="存储展示名称的字段名"
19
+ ></el-input>
20
+ </el-form-item>
21
+ <el-form-item label="输入提示">
22
+ <el-input
23
+ type="text"
24
+ v-model="optionModel.placeholder"
25
+ ></el-input>
26
+ </el-form-item>
27
+ <el-form-item label="可清空">
28
+ <el-switch v-model="optionModel.clearable"></el-switch>
29
+ </el-form-item>
30
+ <el-form-item label="聚焦即查询">
31
+ <el-switch v-model="optionModel.triggerOnFocus"></el-switch>
32
+ </el-form-item>
33
+ <el-form-item label="防抖(ms)">
34
+ <el-input-number
35
+ v-model="optionModel.debounce"
36
+ :min="0"
37
+ :max="2000"
38
+ :step="100"
39
+ controls-position="right"
40
+ style="width: 100%"
41
+ ></el-input-number>
42
+ </el-form-item>
43
+
44
+ <el-form-item label="ID反查脚本编码">
45
+ <el-input
46
+ type="text"
47
+ v-model="optionModel.getByIdScriptCode"
48
+ placeholder="选填,编辑态根据ID反查名称"
49
+ ></el-input>
50
+ </el-form-item>
51
+
52
+ <el-form-item label="清空回调">
53
+ <a
54
+ href="javascript:void(0);"
55
+ class="a-link link-oneLind"
56
+ @click="editEventHandler('onSearchClear', ['rowData', 'rowIndex'])"
57
+ >
58
+ <span>{{ optionModel.onSearchClear }}</span>
59
+ <i class="el-icon-edit"></i>
60
+ </a>
61
+ </el-form-item>
62
+
63
+ <el-form-item label="下拉项模板">
64
+ <a
65
+ href="javascript:void(0);"
66
+ class="a-link link-oneLind"
67
+ @click="editEventHandler('autocompleteItemTemplate', ['item'])"
68
+ >
69
+ <span>{{ optionModel.autocompleteItemTemplate }}</span>
70
+ <i class="el-icon-edit"></i>
71
+ </a>
72
+ </el-form-item>
73
+
74
+ <el-form-item label="选中回填">
75
+ <el-button type="primary" plain round @click="showFillDialog = true">
76
+ {{ i18nt('designer.setting.editAction') }}
77
+ </el-button>
78
+ </el-form-item>
79
+
80
+ <el-dialog
81
+ custom-class="dialog-style list-dialog"
82
+ title="选中回填配置"
83
+ :visible.sync="showFillDialog"
84
+ :modal="false"
85
+ :show-close="true"
86
+ :close-on-click-modal="false"
87
+ :close-on-press-escape="false"
88
+ :destroy-on-close="true"
89
+ top="5vh"
90
+ width="640px"
91
+ v-dialog-drag
92
+ >
93
+ <div class="cont">
94
+ <el-table
95
+ ref="fillTable"
96
+ width="100%"
97
+ height="420"
98
+ :data="fillData"
99
+ border
100
+ stripe
101
+ >
102
+ <el-table-column label="目标字段(组件名)" width="180" prop="targetField">
103
+ <template slot-scope="scope">
104
+ <el-input v-model="scope.row.targetField"></el-input>
105
+ </template>
106
+ </el-table-column>
107
+ <el-table-column label="目标数据属性" width="180" prop="targetFormField">
108
+ <template slot-scope="scope">
109
+ <el-input v-model="scope.row.targetFormField"></el-input>
110
+ </template>
111
+ </el-table-column>
112
+ <el-table-column label="来源字段" width="160" prop="sourceField">
113
+ <template slot-scope="scope">
114
+ <el-input v-model="scope.row.sourceField"></el-input>
115
+ </template>
116
+ </el-table-column>
117
+ <el-table-column label="操作" min-width="60" fixed="right">
118
+ <template #header>
119
+ <el-tooltip
120
+ :hide-after="500"
121
+ class="item"
122
+ effect="dark"
123
+ content="添加"
124
+ placement="top"
125
+ >
126
+ <el-button
127
+ size="mini"
128
+ type=""
129
+ circle
130
+ icon="el-icon-plus"
131
+ @click="fillData.push({})"
132
+ />
133
+ </el-tooltip>
134
+ </template>
135
+ <template #default="{ $index }">
136
+ <el-tooltip effect="dark" content="删除" placement="top">
137
+ <el-button
138
+ size="mini"
139
+ type=""
140
+ circle
141
+ icon="el-icon-delete"
142
+ @click="fillData.splice($index, 1)"
143
+ />
144
+ </el-tooltip>
145
+ </template>
146
+ </el-table-column>
147
+ </el-table>
148
+ </div>
149
+ <div class="dialog-footer" slot="footer">
150
+ <span class="fl tips">注:'目标字段'与'目标数据属性'只需维护一个即可。</span>
151
+ <el-button @click="showFillDialog = false" class="button-sty" icon="el-icon-close">
152
+ {{ i18nt('designer.hint.cancel') }}
153
+ </el-button>
154
+ <el-button type="primary" @click="confirmFillDialog" class="button-sty" icon="el-icon-check">
155
+ {{ i18nt('designer.hint.confirm') }}
156
+ </el-button>
157
+ </div>
158
+ </el-dialog>
159
+ </div>
160
+ </template>
161
+
162
+ <script>
163
+ import i18n from "../../../../../../components/xform/utils/i18n";
164
+ import eventMixin from "../../../../../../components/xform/form-designer/setting-panel/property-editor/event-handler/eventMixin";
165
+ import propertyMixin from "../../../../../../components/xform/form-designer/setting-panel/property-editor/propertyMixin";
166
+
167
+ export default {
168
+ name: "autocomplete-vabSearchName-editor",
169
+ mixins: [i18n, eventMixin, propertyMixin],
170
+ props: {
171
+ designer: Object,
172
+ selectedWidget: Object,
173
+ optionModel: Object,
174
+ },
175
+ data() {
176
+ return {
177
+ showFillDialog: false,
178
+ fillData: [],
179
+ };
180
+ },
181
+ methods: {
182
+ confirmFillDialog() {
183
+ this.optionModel.autocompleteFillConfig = this.$baseLodash.cloneDeep(
184
+ this.fillData
185
+ );
186
+ this.showFillDialog = false;
187
+ },
188
+ },
189
+ watch: {
190
+ showFillDialog(val) {
191
+ if (val) {
192
+ this.fillData = this.$baseLodash.cloneDeep(
193
+ this.optionModel.autocompleteFillConfig || []
194
+ );
195
+ }
196
+ },
197
+ },
198
+ };
199
+ </script>
200
+
201
+ <style scoped></style>
@@ -887,6 +887,57 @@ const vabsearchConfig = {
887
887
  showRules: [],
888
888
  };
889
889
 
890
+ const autocompleteConfig = {
891
+ name: "",
892
+ keyNameEnabled: !1,
893
+ keyName: "",
894
+ //存储"名称"的字段(展示字段),与 ID 字段配合实现"存储ID和名称"
895
+ vabSearchName: "",
896
+ label: "",
897
+ labelColor: "",
898
+ submitFlag: true,
899
+ disabled: !1,
900
+ hidden: !1,
901
+ required: !1,
902
+ labelWidth: null,
903
+ labelHidden: !1,
904
+ ...defaultLabelIconConfig,
905
+ readonly: false,
906
+ size: "",
907
+ widgetWidth: null,
908
+ customClass: [],
909
+ placeholder: "请输入关键词搜索",
910
+ clearable: true,
911
+ //el-autocomplete 相关
912
+ triggerOnFocus: true,
913
+ debounce: 300,
914
+ prefixIcon: null,
915
+ suffixIcon: null,
916
+ //选项数据中"值/ID"对应的字段名(为空时取字段本身 keyName)
917
+ valueField: null,
918
+ //远程搜索后台逻辑脚本配置(复用 httpConfig)
919
+ ...httpConfig,
920
+ formScriptCode: null,
921
+ //通过 ID 反查名称的脚本编码(可选,用于编辑态回显)
922
+ getByIdScriptCode: null,
923
+ //选中后回填其他表单字段的配置
924
+ autocompleteFillConfig: [],
925
+ //自定义下拉项模板脚本(可选,入参 item,返回 HTML 字符串)
926
+ autocompleteItemTemplate: null,
927
+ //事件
928
+ onCreated: "",
929
+ onMounted: "",
930
+ onChange: "",
931
+ onFocus: "",
932
+ onBlur: "",
933
+ onInput: "",
934
+ onSearchClear: "",
935
+ ...defaultWfConfig,
936
+ showRuleFlag: 1,
937
+ showRuleEnabled: 1,
938
+ showRules: [],
939
+ };
940
+
890
941
  const projectTagConfig = {
891
942
  name: "",
892
943
  keyNameEnabled: !1,
@@ -2946,6 +2997,17 @@ export const advancedFields = [
2946
2997
  ...vabsearchConfig,
2947
2998
  },
2948
2999
  },
3000
+ {
3001
+ type: "autocomplete",
3002
+ icon: "searchbox",
3003
+ commonFlag: !0,
3004
+ columnFlag: true,
3005
+ formItemFlag: !0,
3006
+ tableField: null,
3007
+ options: {
3008
+ ...autocompleteConfig,
3009
+ },
3010
+ },
2949
3011
  {
2950
3012
  type: "search_button",
2951
3013
  icon: "button",
@@ -20,7 +20,10 @@ import {
20
20
  isAttachmentWidgetType,
21
21
  isVabsearchMultiWidget,
22
22
  } from "../../../../components/xform/utils/util";
23
- import { applyColumnLabelIcon, resolveColumnRequiredFlag } from "../../../../components/xform/utils/tableColumnHelper";
23
+ import {
24
+ applyColumnLabelIcon,
25
+ resolveColumnRequiredFlag,
26
+ } from "../../../../components/xform/utils/tableColumnHelper";
24
27
  import {
25
28
  buildCellRequiredHint,
26
29
  buildTableCellFormProp,
@@ -314,8 +317,7 @@ modules = {
314
317
  });
315
318
  },
316
319
  initColumnWidgetConfig(callback, options = {}) {
317
- const tableColumns =
318
- options.columns ?? this.getEffectiveTableColumns();
320
+ const tableColumns = options.columns ?? this.getEffectiveTableColumns();
319
321
  const requests = this.collectColumnWidgetConfigRequests(
320
322
  tableColumns,
321
323
  options
@@ -329,8 +331,7 @@ modules = {
329
331
  }
330
332
  },
331
333
  refreshColumnWidgetConfig(options = {}) {
332
- const tableColumns =
333
- options.columns ?? this.getEffectiveTableColumns();
334
+ const tableColumns = options.columns ?? this.getEffectiveTableColumns();
334
335
  const forceRefresh = options.forceRefresh !== false;
335
336
  return new Promise((resolve, reject) => {
336
337
  const requests = this.collectColumnWidgetConfigRequests(tableColumns, {
@@ -346,11 +347,13 @@ modules = {
346
347
  resolve();
347
348
  };
348
349
  if (requests.length) {
349
- Promise.all(requests).then(finish).catch((err) => {
350
- options.fail && options.fail(err);
351
- options.error && options.error(err);
352
- reject(err);
353
- });
350
+ Promise.all(requests)
351
+ .then(finish)
352
+ .catch((err) => {
353
+ options.fail && options.fail(err);
354
+ options.error && options.error(err);
355
+ reject(err);
356
+ });
354
357
  } else {
355
358
  finish();
356
359
  }
@@ -1019,9 +1022,7 @@ modules = {
1019
1022
  });
1020
1023
  },
1021
1024
  shouldSkipDynamicColumnsOnInit() {
1022
- return (
1023
- this.dynamicColumnsInitResolved && this.dynamicColumnsInitSkipped
1024
- );
1025
+ return this.dynamicColumnsInitResolved && this.dynamicColumnsInitSkipped;
1025
1026
  },
1026
1027
  getColumnVisible(columnConfig) {
1027
1028
  if (columnConfig.show === false) {
@@ -1041,34 +1042,37 @@ modules = {
1041
1042
  this.ensureEffectiveColumnWidgets(true);
1042
1043
  const effectiveColumns = this.getEffectiveTableColumns(true);
1043
1044
  return new Promise((resolve) => {
1044
- this.initColumnWidgetConfig(() => {
1045
- const columns = this.createColumns();
1046
- this.widgets = columns
1047
- .filter((column) => !!column?.params?.widget)
1048
- .map((column) => column.params.widget);
1049
- this.editWidgets = columns
1050
- .filter((column) => !!column?.params?.editWidget)
1051
- .map((column) => column.params.editWidget);
1052
- if (this.vxeOption) {
1053
- this.$set(this.vxeOption, "columns", columns);
1054
- }
1055
- const $grid = this.getGridTable();
1056
- if ($grid?.reloadColumn) {
1057
- const reloadResult = $grid.reloadColumn(columns);
1058
- const finish = () => {
1059
- this.initFieldSchemaData(true);
1060
- this.afterDynamicColumnsCommonWidget(effectiveColumns);
1061
- resolve(columns);
1062
- };
1063
- if (reloadResult?.then) {
1064
- reloadResult.then(finish).catch(finish);
1045
+ this.initColumnWidgetConfig(
1046
+ () => {
1047
+ const columns = this.createColumns();
1048
+ this.widgets = columns
1049
+ .filter((column) => !!column?.params?.widget)
1050
+ .map((column) => column.params.widget);
1051
+ this.editWidgets = columns
1052
+ .filter((column) => !!column?.params?.editWidget)
1053
+ .map((column) => column.params.editWidget);
1054
+ if (this.vxeOption) {
1055
+ this.$set(this.vxeOption, "columns", columns);
1056
+ }
1057
+ const $grid = this.getGridTable();
1058
+ if ($grid?.reloadColumn) {
1059
+ const reloadResult = $grid.reloadColumn(columns);
1060
+ const finish = () => {
1061
+ this.initFieldSchemaData(true);
1062
+ this.afterDynamicColumnsCommonWidget(effectiveColumns);
1063
+ resolve(columns);
1064
+ };
1065
+ if (reloadResult?.then) {
1066
+ reloadResult.then(finish).catch(finish);
1067
+ } else {
1068
+ finish();
1069
+ }
1065
1070
  } else {
1066
- finish();
1071
+ resolve(columns);
1067
1072
  }
1068
- } else {
1069
- resolve(columns);
1070
- }
1071
- }, { columns: effectiveColumns, forceRefresh: true });
1073
+ },
1074
+ { columns: effectiveColumns, forceRefresh: true }
1075
+ );
1072
1076
  });
1073
1077
  },
1074
1078
  reloadTableColumns(tableColumns) {
@@ -2819,11 +2823,7 @@ modules = {
2819
2823
  }
2820
2824
  } else {
2821
2825
  if (widget) {
2822
- this.handleWidgetNullValue(
2823
- widget,
2824
- newData,
2825
- defaultValueEnabled
2826
- );
2826
+ this.handleWidgetNullValue(widget, newData, defaultValueEnabled);
2827
2827
  } else {
2828
2828
  newData[item.prop] = null;
2829
2829
  }
@@ -3086,7 +3086,6 @@ modules = {
3086
3086
  let formCode = reportTemplate?.formCode;
3087
3087
  let scriptCode = formConfig.saveScriptCode || "saveUpdate";
3088
3088
 
3089
- let that = this;
3090
3089
  let $grid = obj.$table.$xegrid;
3091
3090
  let originOption = $grid.params.originOption;
3092
3091
 
@@ -3138,20 +3137,10 @@ modules = {
3138
3137
  confirmText: "您确定要保存吗?",
3139
3138
  success: (res0) => {
3140
3139
  this.getRowData(res0.objx, (res) => {
3141
- if (obj.row.id === res.objx.id) {
3142
- $grid.clearActived().then(() => {
3143
- Object.assign(obj.row, res.objx);
3144
- });
3145
- } else {
3146
- // $grid.remove(obj.row);
3147
- // $grid.insertAt(res.objx);
3148
- let items = that.getValue();
3149
- let index = items.findIndex(
3150
- (item) => item.id === obj.row.id
3151
- );
3152
- items.splice(index, 1, res.objx);
3153
- that.setValue(items);
3154
- }
3140
+ $grid.clearActived().then(() => {
3141
+ // 保留 vxe 内部行引用和 _X_ROW_KEY,仅同步服务端字段。
3142
+ Object.assign(obj.row, res.objx);
3143
+ });
3155
3144
  delete obj.$table.editCloneRow;
3156
3145
  });
3157
3146
  },
@@ -3243,7 +3232,7 @@ modules = {
3243
3232
  this.$nextTick(() => {
3244
3233
  this.handleWbs();
3245
3234
  if (isEditTable) {
3246
- $grid.setActiveRow(newRow);
3235
+ this.setActiveRowById($grid, newRow.id);
3247
3236
  }
3248
3237
  });
3249
3238
 
@@ -3253,10 +3242,9 @@ modules = {
3253
3242
  if (toEnd === true) {
3254
3243
  tableRows.push(newRow);
3255
3244
  } else if (toSibling === true) {
3256
- let addIndex =
3257
- tableRows.findIndex(
3258
- (item) => item._X_ROW_KEY === obj.row._X_ROW_KEY
3259
- ) + 1;
3245
+ let addIndex = tableRows.findIndex(
3246
+ (item) => item._X_ROW_KEY === obj.row._X_ROW_KEY
3247
+ ) + 1;
3260
3248
  tableRows.splice(addIndex, 0, newRow);
3261
3249
  } else {
3262
3250
  tableRows.splice(0, 0, newRow);
@@ -3266,7 +3254,7 @@ modules = {
3266
3254
  this.handleWbs();
3267
3255
  $grid.setTreeExpand(obj.row, true).then(() => {
3268
3256
  if (isEditTable) {
3269
- $grid.setActiveRow(newRow);
3257
+ this.setActiveRowById($grid, newRow.id);
3270
3258
  }
3271
3259
  });
3272
3260
  });
@@ -3276,40 +3264,50 @@ modules = {
3276
3264
  removeTreeRow(obj) {
3277
3265
  let row = obj.row;
3278
3266
  let $grid = this.getGridTable();
3267
+ let isTreeTable = this.widget.options.isTreeTable || false;
3279
3268
  let childrenField = $grid.treeConfig?.children;
3280
- // let tableRows = this.formModel[this.fieldKeyName];
3281
- // let index = tableRows.findIndex(item => item.id === row.id)
3282
- let delIds = [];
3269
+ let tableRows = this.getValue() || [];
3270
+ let delKeys = new Set();
3271
+ // 收集当前行;若数据是嵌套结构,同时收集 children 下的子节点
3283
3272
  let loopDo = (item) => {
3284
- if (item._X_ROW_KEY) delIds.push(item._X_ROW_KEY);
3285
- if (childrenField && item[childrenField]) {
3273
+ if (item && item._X_ROW_KEY) delKeys.add(item._X_ROW_KEY);
3274
+ if (childrenField && item && item[childrenField]) {
3286
3275
  item[childrenField].forEach((subItem) => {
3287
3276
  loopDo(subItem);
3288
3277
  });
3289
3278
  }
3290
3279
  };
3291
3280
  loopDo(row);
3292
- /* delIds.forEach((id) => {
3293
- let index = tableRows.findIndex((item) => item.id === id);
3294
- let delRow = tableRows[index];
3295
- tableRows.splice(index, 1);
3296
- this.deleteRowWidgets(delRow);
3297
- }); */
3298
- let tableRows = this.getValue();
3299
- let delIndex = [];
3300
- tableRows.map((item, index) => {
3301
- if (delIds.includes(item._X_ROW_KEY)) {
3302
- delIndex.push(index);
3281
+ // 树表使用扁平数据(transform),子节点通过 parentField 关联,
3282
+ // 需按 parentField 递归收集所有后代,避免删除父节点后子节点残留
3283
+ if (isTreeTable && $grid.treeConfig) {
3284
+ let parentField = $grid.treeConfig.parentField;
3285
+ let rowField = $grid.treeConfig.rowField || "id";
3286
+ let pending = [row];
3287
+ while (pending.length) {
3288
+ let cur = pending.shift();
3289
+ let curId = cur[rowField];
3290
+ tableRows.forEach((item) => {
3291
+ if (!item || !item._X_ROW_KEY || delKeys.has(item._X_ROW_KEY)) {
3292
+ return;
3293
+ }
3294
+ let parentValue = item[parentField];
3295
+ let matched =
3296
+ parentValue === curId ||
3297
+ (parentValue != null &&
3298
+ curId != null &&
3299
+ String(parentValue) === String(curId));
3300
+ if (matched) {
3301
+ delKeys.add(item._X_ROW_KEY);
3302
+ pending.push(item);
3303
+ }
3304
+ });
3303
3305
  }
3304
- });
3305
- delIndex.reverse().forEach((index) => {
3306
- tableRows.splice(index, 1);
3307
- });
3308
-
3309
- // $grid.remove(obj.row);
3310
-
3311
- // let newRows = tableRows.filter((item) => !delIds.includes(item.id));
3312
- // this.setValue(newRows);
3306
+ }
3307
+ let newRows = tableRows.filter((item) => !delKeys.has(item._X_ROW_KEY));
3308
+ // 通过 setValue 重新赋值数据源,触发 vxe-grid 重新加载。
3309
+ // 树表(transform)下直接 splice 原数组不会刷新视图,会导致“删除无反应”。
3310
+ this.setValue(newRows);
3313
3311
  this.$nextTick(() => {
3314
3312
  this.handleWbs();
3315
3313
  });
@@ -3430,6 +3428,55 @@ modules = {
3430
3428
  newRow = Object.assign({}, newData, newRow, editDefaultRow, rowData);
3431
3429
 
3432
3430
  let tableRows = this.formModel[this.fieldKeyName] || [];
3431
+ // 编辑表统一使用 vxe 内部插入,避免 setValue 重载整表并丢失编辑状态。
3432
+ if (isEditTable) {
3433
+ let insertTarget = toEnd === true ? -1 : null;
3434
+ if (toSibling === true) {
3435
+ let currentIndex = tableRows.findIndex(
3436
+ (item) => item._X_ROW_KEY === obj.row._X_ROW_KEY
3437
+ );
3438
+ if (isTreeTable) {
3439
+ let parentField = $grid.treeConfig.parentField;
3440
+ let nextSibling = tableRows
3441
+ .slice(currentIndex + 1)
3442
+ .find(
3443
+ (item) => item[parentField] === obj.row[parentField]
3444
+ );
3445
+ insertTarget = nextSibling || -1;
3446
+ } else {
3447
+ insertTarget = tableRows[currentIndex + 1] || -1;
3448
+ }
3449
+ }
3450
+ $grid.insertAt(newRow, insertTarget).then(({ row }) => {
3451
+ let nextRows = tableRows.slice();
3452
+ if (toEnd === true) {
3453
+ nextRows.push(row);
3454
+ } else if (toSibling === true) {
3455
+ let addIndex = nextRows.findIndex(
3456
+ (item) => item._X_ROW_KEY === obj.row._X_ROW_KEY
3457
+ ) + 1;
3458
+ nextRows.splice(addIndex, 0, row);
3459
+ } else {
3460
+ nextRows.splice(0, 0, row);
3461
+ }
3462
+ // 不修改绑定给 vxe-grid 的 fieldModel,避免数组监听触发整表重载。
3463
+ this.formModel[this.fieldKeyName] = nextRows;
3464
+ this.initRowIdData();
3465
+ this.initFieldSchemaData();
3466
+ this.$nextTick(() => {
3467
+ Promise.resolve(this.handleWbs()).then(() => {
3468
+ if (isTreeTable && parent) {
3469
+ $grid.setTreeExpand(parent, true).then(() => {
3470
+ $grid.setActiveRow(row);
3471
+ });
3472
+ } else {
3473
+ $grid.setActiveRow(row);
3474
+ }
3475
+ });
3476
+ });
3477
+ });
3478
+ return;
3479
+ }
3433
3480
  if (!parent) {
3434
3481
  if (toEnd === true) {
3435
3482
  tableRows.push(newRow);
@@ -3445,12 +3492,7 @@ modules = {
3445
3492
  this.setValue(tableRows);
3446
3493
  this.$nextTick(() => {
3447
3494
  this.handleWbs();
3448
- if (isEditTable) {
3449
- $grid.setActiveRow(newRow);
3450
- }
3451
3495
  });
3452
-
3453
- // $grid.insert(newRow).then(({row}) => $grid.setActiveRow(row));
3454
3496
  } else {
3455
3497
  $grid.setTreeExpand(obj.row, true).then(() => {
3456
3498
  if (toEnd === true) {
@@ -3467,17 +3509,50 @@ modules = {
3467
3509
  this.setValue(tableRows);
3468
3510
  this.$nextTick(() => {
3469
3511
  this.handleWbs();
3470
- $grid.setTreeExpand(obj.row, true).then(() => {
3471
- if (isEditTable) {
3472
- $grid.setActiveRow(newRow);
3473
- }
3474
- });
3512
+ $grid.setTreeExpand(obj.row, true);
3475
3513
  });
3476
3514
  });
3477
3515
  }
3478
3516
  },
3517
+ // setValue 会生成新的数据行并触发 vxe 异步重载,需等待内部行映射更新后再激活。
3518
+ setActiveRowById($grid, rowId, retry = 0) {
3519
+ if (!$grid || rowId == null) {
3520
+ return;
3521
+ }
3522
+ let sourceRow = (this.getValue() || []).find(
3523
+ (item) => item && item.id === rowId
3524
+ );
3525
+ if (!sourceRow) {
3526
+ return;
3527
+ }
3528
+ // vxe-table 使用 _X_ROW_KEY 缓存内部行对象。树数据重载后,sourceRow
3529
+ // 与表格内部渲染的行并非同一引用,必须从 vxe 的行映射中重新获取。
3530
+ let realRow = $grid.getRowById
3531
+ ? $grid.getRowById(sourceRow._X_ROW_KEY)
3532
+ : sourceRow;
3533
+ if (!realRow && retry < 10) {
3534
+ this.$nextTick(() => {
3535
+ this.setActiveRowById($grid, rowId, retry + 1);
3536
+ });
3537
+ return;
3538
+ }
3539
+ if (realRow) {
3540
+ $grid.setActiveRow(realRow);
3541
+ }
3542
+ },
3479
3543
  async removeEditRow(obj) {
3480
- this.removeTreeRow(obj);
3544
+ let row = obj.row;
3545
+ let $grid = this.getGridTable();
3546
+ await $grid.remove(row);
3547
+
3548
+ // 临时编辑行由 vxe 内部删除;仅同步表单模型,避免 setValue 重载整表。
3549
+ let tableRows = this.getValue() || [];
3550
+ this.formModel[this.fieldKeyName] = tableRows.filter(
3551
+ (item) => item._X_ROW_KEY !== row._X_ROW_KEY
3552
+ );
3553
+ this.initRowIdData();
3554
+ this.initFieldSchemaData();
3555
+ this.handleWbs();
3481
3556
  },
3482
3557
  //editTable end
3483
3558
  getHttpConfigForUser() {
@@ -3489,7 +3564,7 @@ modules = {
3489
3564
  },
3490
3565
  handleWbs() {
3491
3566
  if (this.widget.options.wbsEnabled) {
3492
- this.updateWbs();
3567
+ return this.updateWbs();
3493
3568
  }
3494
3569
  },
3495
3570
  updateWbs() {
@@ -3497,6 +3572,15 @@ modules = {
3497
3572
  let $grid = that.getGridTable();
3498
3573
  let childrenField = $grid.treeConfig.children;
3499
3574
  let fullData = $grid.getTableData().fullData;
3575
+ let isEditTable = this.widget.options.isEditTable || false;
3576
+ let isTreeTable = this.widget.options.isTreeTable || false;
3577
+ if (
3578
+ isEditTable
3579
+ && isTreeTable
3580
+ && $grid.$refs?.xTable?.tableFullTreeData
3581
+ ) {
3582
+ fullData = $grid.$refs.xTable.tableFullTreeData;
3583
+ }
3500
3584
  let tableRows = this.formModel[that.fieldKeyName] || [];
3501
3585
  let map = {};
3502
3586
  let loopDo = (item, wbs) => {
@@ -3512,7 +3596,14 @@ modules = {
3512
3596
  let wbs = index + 1 + "";
3513
3597
  loopDo(item, wbs);
3514
3598
  });
3515
- $grid.updateData();
3599
+ if (isEditTable) {
3600
+ tableRows.forEach((item) => {
3601
+ if (Object.prototype.hasOwnProperty.call(map, item._X_ROW_KEY)) {
3602
+ item.f_wbs = map[item._X_ROW_KEY];
3603
+ }
3604
+ });
3605
+ }
3606
+ return $grid.updateData();
3516
3607
  },
3517
3608
  moveUpRow(obj) {
3518
3609
  let row = obj.row;
@@ -58,7 +58,8 @@ export default {
58
58
  cascader: "Cascader",
59
59
  "area-select": "Area Select",
60
60
  slot: "Slot",
61
- custom: "Custom Component"
61
+ custom: "Custom Component",
62
+ autocomplete: "Autocomplete"
62
63
  },
63
64
  hint: {
64
65
  selectParentWidget: "Select parent of this widget",
@@ -65,6 +65,7 @@ export default {
65
65
  vabUpload: "凭证",
66
66
  vabUpload2: "凭证(海信)",
67
67
  vabsearch: "搜索框",
68
+ autocomplete: "自动完成",
68
69
  search_button: "搜索按钮",
69
70
  save_button: "保存按钮",
70
71
  reset_button: "刷新按钮",