@tmagic/form 1.8.0-beta.15 → 1.8.0-beta.17

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.
@@ -145,6 +145,17 @@ var Form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
145
145
  }
146
146
  });
147
147
  /**
148
+ * formState 的内置 key 快照(keyProp / values / $emit / fields / post 等)。
149
+ *
150
+ * 在 `extendState` 首次合并前捕获,`applyExtendState` 会据此禁止 `extendState`
151
+ * 覆盖这些已有字段(只能新增字段),避免表单核心状态被外部意外改写。
152
+ *
153
+ * 之所以在此处(effect 之外)捕获而不是在 `applyExtendState` 内动态取:
154
+ * `watchEffect` 会在依赖变化时重跑,若动态取,`extendState` 自己新增的字段在第二次
155
+ * 合并时也会被当成「已有 key」而拒绝刷新;这里只锁定内置字段即可规避该问题。
156
+ */
157
+ const reservedStateKeys = new Set(Reflect.ownKeys(formState));
158
+ /**
148
159
  * `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
149
160
  * 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
150
161
  *
@@ -179,7 +190,7 @@ var Form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
179
190
  return;
180
191
  }
181
192
  if (stale) return;
182
- applyExtendState(formState, state);
193
+ applyExtendState(formState, state, reservedStateKeys);
183
194
  });
184
195
  provide("mForm", formState);
185
196
  /**
package/dist/es/style.css CHANGED
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
296
296
  gap: 8px;
297
297
  }
298
298
  .m-fields-group-list .m-fields-group-list-footer {
299
- display: flex;
300
299
  justify-content: space-between;
301
300
  margin-top: 10px;
302
301
  margin-bottom: 16px;
303
302
  }
304
303
 
304
+ /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
305
+ .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
306
+ margin-bottom: 10px;
307
+ }
308
+
305
309
  .m-form-panel .el-card__header:hover {
306
310
  background: #f2f6fc;
307
311
  }
@@ -3,6 +3,8 @@ import { applyExtendState } from "./utils/form.js";
3
3
  import Form_default from "./Form.js";
4
4
  import { createApp, defineComponent, h, nextTick, provide, ref, watch } from "vue";
5
5
  //#region packages/form/src/submitForm.ts
6
+ /** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
7
+ var DEFAULT_MOUNT_TIMEOUT = 1e4;
6
8
  /**
7
9
  * submitForm / validateForm 的公共脚手架:
8
10
  *
@@ -13,14 +15,19 @@ import { createApp, defineComponent, h, nextTick, provide, ref, watch } from "vu
13
15
  * 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
14
16
  */
15
17
  var mountFormInstance = (options) => {
16
- const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options;
18
+ const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
17
19
  return new Promise((resolve, reject) => {
18
- const container = document.createElement("div");
19
- if (hidden) container.style.display = "none";
20
- document.body.appendChild(container);
20
+ if (signal?.aborted) {
21
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
22
+ return;
23
+ }
21
24
  let cleaned = false;
22
25
  let timer = null;
26
+ let onAbort = null;
23
27
  const instance = { app: null };
28
+ const container = document.createElement("div");
29
+ if (hidden) container.style.display = "none";
30
+ document.body.appendChild(container);
24
31
  const cleanup = () => {
25
32
  if (cleaned) return;
26
33
  cleaned = true;
@@ -28,58 +35,71 @@ var mountFormInstance = (options) => {
28
35
  clearTimeout(timer);
29
36
  timer = null;
30
37
  }
38
+ if (signal && onAbort) {
39
+ signal.removeEventListener("abort", onAbort);
40
+ onAbort = null;
41
+ }
31
42
  try {
32
43
  instance.app?.unmount();
33
44
  } catch {}
34
45
  container.parentNode?.removeChild(container);
35
46
  };
36
- const formRef = ref(null);
37
- const { extendState, ...restFormProps } = formProps;
38
- const userWrapper = createWrapper({
39
- formRef,
40
- formProps: restFormProps,
41
- cleanup,
42
- resolve,
43
- reject
44
- });
45
- const wrapperComponent = typeof extendState === "function" ? defineComponent({
46
- name: "MFormExtendStateInjector",
47
- setup() {
48
- watch(() => formRef.value, (form) => {
49
- if (!form) return;
50
- let result;
51
- try {
52
- result = extendState(form.formState);
53
- } catch (e) {
54
- console.error("[MForm] extendState failed:", e);
55
- return;
56
- }
57
- const apply = (state) => applyExtendState(form.formState, state);
58
- if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
59
- else apply(result);
60
- }, {
61
- flush: "sync",
62
- immediate: true
63
- });
64
- return () => h(userWrapper);
65
- }
66
- }) : userWrapper;
67
- const app = createApp(hidden ? defineComponent({
68
- name: "MFormSilentProvider",
69
- setup() {
70
- provide(FORM_SILENT_MODE_KEY, true);
71
- return () => h(wrapperComponent);
72
- }
73
- }) : wrapperComponent);
74
- instance.app = app;
75
- if (appContext) Object.assign(app._context, appContext);
76
- if (timeout > 0 && !skipTimeout) timer = setTimeout(() => {
77
- if (!cleaned) {
78
- reject(new Error(timeoutMessage));
47
+ if (signal) {
48
+ onAbort = () => {
49
+ if (cleaned) return;
50
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
79
51
  cleanup();
80
- }
81
- }, timeout);
52
+ };
53
+ signal.addEventListener("abort", onAbort);
54
+ }
82
55
  try {
56
+ const formRef = ref(null);
57
+ const { extendState, ...restFormProps } = formProps;
58
+ const userWrapper = createWrapper({
59
+ formRef,
60
+ formProps: restFormProps,
61
+ cleanup,
62
+ resolve,
63
+ reject
64
+ });
65
+ const wrapperComponent = typeof extendState === "function" ? defineComponent({
66
+ name: "MFormExtendStateInjector",
67
+ setup() {
68
+ watch(() => formRef.value, (form) => {
69
+ if (!form) return;
70
+ let result;
71
+ try {
72
+ result = extendState(form.formState);
73
+ } catch (e) {
74
+ console.error("[MForm] extendState failed:", e);
75
+ return;
76
+ }
77
+ const reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
78
+ const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
79
+ if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
80
+ else apply(result);
81
+ }, {
82
+ flush: "sync",
83
+ immediate: true
84
+ });
85
+ return () => h(userWrapper);
86
+ }
87
+ }) : userWrapper;
88
+ const app = createApp(hidden ? defineComponent({
89
+ name: "MFormSilentProvider",
90
+ setup() {
91
+ provide(FORM_SILENT_MODE_KEY, true);
92
+ return () => h(wrapperComponent);
93
+ }
94
+ }) : wrapperComponent);
95
+ instance.app = app;
96
+ if (appContext) Object.assign(app._context, appContext);
97
+ if (!skipTimeout) timer = setTimeout(() => {
98
+ if (!cleaned) {
99
+ reject(new Error(timeoutMessage));
100
+ cleanup();
101
+ }
102
+ }, timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT);
83
103
  app.mount(container);
84
104
  } catch (err) {
85
105
  reject(err);
@@ -212,11 +232,12 @@ var createDebugWrapper = (options) => {
212
232
  * ```
213
233
  */
214
234
  var submitForm = (options) => {
215
- const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, ...formProps } = options;
235
+ const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
216
236
  return mountFormInstance({
217
237
  formProps,
218
238
  appContext,
219
239
  timeout,
240
+ signal,
220
241
  hidden: !debug,
221
242
  skipTimeout: debug,
222
243
  timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
@@ -353,7 +374,7 @@ var stripTabItemsLazy = (config) => {
353
374
  * ```
354
375
  */
355
376
  var validateForm = (options) => {
356
- const { appContext, timeout = 1e4, debug = false, config, ...rest } = options;
377
+ const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
357
378
  return mountFormInstance({
358
379
  formProps: {
359
380
  ...rest,
@@ -361,6 +382,7 @@ var validateForm = (options) => {
361
382
  },
362
383
  appContext,
363
384
  timeout,
385
+ signal,
364
386
  hidden: !debug,
365
387
  skipTimeout: debug,
366
388
  timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
@@ -211,15 +211,19 @@ var sortChange = (data, { prop, order }) => {
211
211
  * - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
212
212
  * 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
213
213
  *
214
- * 注意:formState 上由 props 派生的字段(keyProp / popperClass / config / initValues /
215
- * isCompare / lastValues / parentValues)是只读 getter(无 setter),extendState 若以
216
- * 普通字段形式返回同名 key,直接赋值会让 proxy set trap 失败并抛出
217
- * `TypeError: 'set' on proxy: trap returned falsish`,这里统一跳过并告警;
218
- * 如确需覆盖,可在 extendState 中以 get 访问器形式返回。
214
+ * 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
215
+ * 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
216
+ * config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
217
+ * post 等),命中这些 key 时统一跳过并告警。
218
+ *
219
+ * 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
220
+ * 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
221
+ * `TypeError: 'set' on proxy: trap returned falsish`。
219
222
  */
220
- var applyExtendState = (formState, state) => {
223
+ var applyExtendState = (formState, state, reservedKeys) => {
221
224
  if (!state) return;
222
225
  for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
226
+ if (reservedKeys?.has(key)) continue;
223
227
  if (!("value" in descriptor)) {
224
228
  descriptor.configurable = true;
225
229
  Object.defineProperty(formState, key, descriptor);
@@ -89,7 +89,7 @@ var stringifyExampleValue = (value) => {
89
89
  }
90
90
  return String(value);
91
91
  };
92
- var MAX_SUGGESTION_OPTIONS = 5;
92
+ var MAX_SUGGESTION_OPTIONS = 20;
93
93
  /**
94
94
  * 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
95
95
  * 可选值超过 MAX_SUGGESTION_OPTIONS 个时仅展示前若干个并以「等」省略。
package/dist/style.css CHANGED
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
296
296
  gap: 8px;
297
297
  }
298
298
  .m-fields-group-list .m-fields-group-list-footer {
299
- display: flex;
300
299
  justify-content: space-between;
301
300
  margin-top: 10px;
302
301
  margin-bottom: 16px;
303
302
  }
304
303
 
304
+ /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
305
+ .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
306
+ margin-bottom: 10px;
307
+ }
308
+
305
309
  .m-form-panel .el-card__header:hover {
306
310
  background: #f2f6fc;
307
311
  }
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
296
296
  gap: 8px;
297
297
  }
298
298
  .m-fields-group-list .m-fields-group-list-footer {
299
- display: flex;
300
299
  justify-content: space-between;
301
300
  margin-top: 10px;
302
301
  margin-bottom: 16px;
303
302
  }
304
303
 
304
+ /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
305
+ .m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
306
+ margin-bottom: 10px;
307
+ }
308
+
305
309
  .m-form-panel .el-card__header:hover {
306
310
  background: #f2f6fc;
307
311
  }
@@ -496,4 +500,10 @@ fieldset.m-fieldset .m-form-tip {
496
500
  }
497
501
  .m-form.m-form--magic-admin .m-form-container .m-form-container-expand {
498
502
  text-align: left;
503
+ }
504
+ .m-form.m-form--magic-admin .m-fields-group-list .el-table__empty-block .el-table__empty-text {
505
+ width: 100%;
506
+ background-color: rgba(0, 0, 0, 0.03);
507
+ margin-top: 8px;
508
+ border-radius: 4px;
499
509
  }
@@ -2816,7 +2816,7 @@
2816
2816
  }
2817
2817
  return String(value);
2818
2818
  };
2819
- var MAX_SUGGESTION_OPTIONS = 5;
2819
+ var MAX_SUGGESTION_OPTIONS = 20;
2820
2820
  /**
2821
2821
  * 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
2822
2822
  * 可选值超过 MAX_SUGGESTION_OPTIONS 个时仅展示前若干个并以「等」省略。
@@ -3340,15 +3340,19 @@
3340
3340
  * - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
3341
3341
  * 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
3342
3342
  *
3343
- * 注意:formState 上由 props 派生的字段(keyProp / popperClass / config / initValues /
3344
- * isCompare / lastValues / parentValues)是只读 getter(无 setter),extendState 若以
3345
- * 普通字段形式返回同名 key,直接赋值会让 proxy set trap 失败并抛出
3346
- * `TypeError: 'set' on proxy: trap returned falsish`,这里统一跳过并告警;
3347
- * 如确需覆盖,可在 extendState 中以 get 访问器形式返回。
3343
+ * 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
3344
+ * 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
3345
+ * config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
3346
+ * post 等),命中这些 key 时统一跳过并告警。
3347
+ *
3348
+ * 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
3349
+ * 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
3350
+ * `TypeError: 'set' on proxy: trap returned falsish`。
3348
3351
  */
3349
- var applyExtendState = (formState, state) => {
3352
+ var applyExtendState = (formState, state, reservedKeys) => {
3350
3353
  if (!state) return;
3351
3354
  for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
3355
+ if (reservedKeys?.has(key)) continue;
3352
3356
  if (!("value" in descriptor)) {
3353
3357
  descriptor.configurable = true;
3354
3358
  Object.defineProperty(formState, key, descriptor);
@@ -4167,6 +4171,17 @@
4167
4171
  }
4168
4172
  });
4169
4173
  /**
4174
+ * formState 的内置 key 快照(keyProp / values / $emit / fields / post 等)。
4175
+ *
4176
+ * 在 `extendState` 首次合并前捕获,`applyExtendState` 会据此禁止 `extendState`
4177
+ * 覆盖这些已有字段(只能新增字段),避免表单核心状态被外部意外改写。
4178
+ *
4179
+ * 之所以在此处(effect 之外)捕获而不是在 `applyExtendState` 内动态取:
4180
+ * `watchEffect` 会在依赖变化时重跑,若动态取,`extendState` 自己新增的字段在第二次
4181
+ * 合并时也会被当成「已有 key」而拒绝刷新;这里只锁定内置字段即可规避该问题。
4182
+ */
4183
+ const reservedStateKeys = new Set(Reflect.ownKeys(formState));
4184
+ /**
4170
4185
  * `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
4171
4186
  * 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
4172
4187
  *
@@ -4201,7 +4216,7 @@
4201
4216
  return;
4202
4217
  }
4203
4218
  if (stale) return;
4204
- applyExtendState(formState, state);
4219
+ applyExtendState(formState, state, reservedStateKeys);
4205
4220
  });
4206
4221
  (0, vue.provide)("mForm", formState);
4207
4222
  /**
@@ -4405,6 +4420,8 @@
4405
4420
  });
4406
4421
  //#endregion
4407
4422
  //#region packages/form/src/submitForm.ts
4423
+ /** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
4424
+ var DEFAULT_MOUNT_TIMEOUT = 1e4;
4408
4425
  /**
4409
4426
  * submitForm / validateForm 的公共脚手架:
4410
4427
  *
@@ -4415,14 +4432,19 @@
4415
4432
  * 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
4416
4433
  */
4417
4434
  var mountFormInstance = (options) => {
4418
- const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options;
4435
+ const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
4419
4436
  return new Promise((resolve, reject) => {
4420
- const container = document.createElement("div");
4421
- if (hidden) container.style.display = "none";
4422
- document.body.appendChild(container);
4437
+ if (signal?.aborted) {
4438
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
4439
+ return;
4440
+ }
4423
4441
  let cleaned = false;
4424
4442
  let timer = null;
4443
+ let onAbort = null;
4425
4444
  const instance = { app: null };
4445
+ const container = document.createElement("div");
4446
+ if (hidden) container.style.display = "none";
4447
+ document.body.appendChild(container);
4426
4448
  const cleanup = () => {
4427
4449
  if (cleaned) return;
4428
4450
  cleaned = true;
@@ -4430,58 +4452,71 @@
4430
4452
  clearTimeout(timer);
4431
4453
  timer = null;
4432
4454
  }
4455
+ if (signal && onAbort) {
4456
+ signal.removeEventListener("abort", onAbort);
4457
+ onAbort = null;
4458
+ }
4433
4459
  try {
4434
4460
  instance.app?.unmount();
4435
4461
  } catch {}
4436
4462
  container.parentNode?.removeChild(container);
4437
4463
  };
4438
- const formRef = (0, vue.ref)(null);
4439
- const { extendState, ...restFormProps } = formProps;
4440
- const userWrapper = createWrapper({
4441
- formRef,
4442
- formProps: restFormProps,
4443
- cleanup,
4444
- resolve,
4445
- reject
4446
- });
4447
- const wrapperComponent = typeof extendState === "function" ? (0, vue.defineComponent)({
4448
- name: "MFormExtendStateInjector",
4449
- setup() {
4450
- (0, vue.watch)(() => formRef.value, (form) => {
4451
- if (!form) return;
4452
- let result;
4453
- try {
4454
- result = extendState(form.formState);
4455
- } catch (e) {
4456
- console.error("[MForm] extendState failed:", e);
4457
- return;
4458
- }
4459
- const apply = (state) => applyExtendState(form.formState, state);
4460
- if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
4461
- else apply(result);
4462
- }, {
4463
- flush: "sync",
4464
- immediate: true
4465
- });
4466
- return () => (0, vue.h)(userWrapper);
4467
- }
4468
- }) : userWrapper;
4469
- const app = (0, vue.createApp)(hidden ? (0, vue.defineComponent)({
4470
- name: "MFormSilentProvider",
4471
- setup() {
4472
- (0, vue.provide)(FORM_SILENT_MODE_KEY, true);
4473
- return () => (0, vue.h)(wrapperComponent);
4474
- }
4475
- }) : wrapperComponent);
4476
- instance.app = app;
4477
- if (appContext) Object.assign(app._context, appContext);
4478
- if (timeout > 0 && !skipTimeout) timer = setTimeout(() => {
4479
- if (!cleaned) {
4480
- reject(new Error(timeoutMessage));
4464
+ if (signal) {
4465
+ onAbort = () => {
4466
+ if (cleaned) return;
4467
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
4481
4468
  cleanup();
4482
- }
4483
- }, timeout);
4469
+ };
4470
+ signal.addEventListener("abort", onAbort);
4471
+ }
4484
4472
  try {
4473
+ const formRef = (0, vue.ref)(null);
4474
+ const { extendState, ...restFormProps } = formProps;
4475
+ const userWrapper = createWrapper({
4476
+ formRef,
4477
+ formProps: restFormProps,
4478
+ cleanup,
4479
+ resolve,
4480
+ reject
4481
+ });
4482
+ const wrapperComponent = typeof extendState === "function" ? (0, vue.defineComponent)({
4483
+ name: "MFormExtendStateInjector",
4484
+ setup() {
4485
+ (0, vue.watch)(() => formRef.value, (form) => {
4486
+ if (!form) return;
4487
+ let result;
4488
+ try {
4489
+ result = extendState(form.formState);
4490
+ } catch (e) {
4491
+ console.error("[MForm] extendState failed:", e);
4492
+ return;
4493
+ }
4494
+ const reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
4495
+ const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
4496
+ if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
4497
+ else apply(result);
4498
+ }, {
4499
+ flush: "sync",
4500
+ immediate: true
4501
+ });
4502
+ return () => (0, vue.h)(userWrapper);
4503
+ }
4504
+ }) : userWrapper;
4505
+ const app = (0, vue.createApp)(hidden ? (0, vue.defineComponent)({
4506
+ name: "MFormSilentProvider",
4507
+ setup() {
4508
+ (0, vue.provide)(FORM_SILENT_MODE_KEY, true);
4509
+ return () => (0, vue.h)(wrapperComponent);
4510
+ }
4511
+ }) : wrapperComponent);
4512
+ instance.app = app;
4513
+ if (appContext) Object.assign(app._context, appContext);
4514
+ if (!skipTimeout) timer = setTimeout(() => {
4515
+ if (!cleaned) {
4516
+ reject(new Error(timeoutMessage));
4517
+ cleanup();
4518
+ }
4519
+ }, timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT);
4485
4520
  app.mount(container);
4486
4521
  } catch (err) {
4487
4522
  reject(err);
@@ -4614,11 +4649,12 @@
4614
4649
  * ```
4615
4650
  */
4616
4651
  var submitForm = (options) => {
4617
- const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, ...formProps } = options;
4652
+ const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
4618
4653
  return mountFormInstance({
4619
4654
  formProps,
4620
4655
  appContext,
4621
4656
  timeout,
4657
+ signal,
4622
4658
  hidden: !debug,
4623
4659
  skipTimeout: debug,
4624
4660
  timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
@@ -4755,7 +4791,7 @@
4755
4791
  * ```
4756
4792
  */
4757
4793
  var validateForm = (options) => {
4758
- const { appContext, timeout = 1e4, debug = false, config, ...rest } = options;
4794
+ const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
4759
4795
  return mountFormInstance({
4760
4796
  formProps: {
4761
4797
  ...rest,
@@ -4763,6 +4799,7 @@
4763
4799
  },
4764
4800
  appContext,
4765
4801
  timeout,
4802
+ signal,
4766
4803
  hidden: !debug,
4767
4804
  skipTimeout: debug,
4768
4805
  timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.15",
2
+ "version": "1.8.0-beta.17",
3
3
  "name": "@tmagic/form",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -52,9 +52,9 @@
52
52
  "peerDependencies": {
53
53
  "vue": "^3.5.40",
54
54
  "typescript": "^6.0.3",
55
- "@tmagic/design": "1.8.0-beta.15",
56
- "@tmagic/form-schema": "1.8.0-beta.15",
57
- "@tmagic/utils": "1.8.0-beta.15"
55
+ "@tmagic/design": "1.8.0-beta.17",
56
+ "@tmagic/utils": "1.8.0-beta.17",
57
+ "@tmagic/form-schema": "1.8.0-beta.17"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "typescript": {
package/src/Form.vue CHANGED
@@ -261,6 +261,18 @@ const formState: FormState = reactive<FormState>({
261
261
  },
262
262
  });
263
263
 
264
+ /**
265
+ * formState 的内置 key 快照(keyProp / values / $emit / fields / post 等)。
266
+ *
267
+ * 在 `extendState` 首次合并前捕获,`applyExtendState` 会据此禁止 `extendState`
268
+ * 覆盖这些已有字段(只能新增字段),避免表单核心状态被外部意外改写。
269
+ *
270
+ * 之所以在此处(effect 之外)捕获而不是在 `applyExtendState` 内动态取:
271
+ * `watchEffect` 会在依赖变化时重跑,若动态取,`extendState` 自己新增的字段在第二次
272
+ * 合并时也会被当成「已有 key」而拒绝刷新;这里只锁定内置字段即可规避该问题。
273
+ */
274
+ const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(formState));
275
+
264
276
  /**
265
277
  * `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
266
278
  * 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
@@ -299,7 +311,7 @@ watchEffect(async (onCleanup) => {
299
311
  }
300
312
  if (stale) return;
301
313
 
302
- applyExtendState(formState, state);
314
+ applyExtendState(formState, state, reservedStateKeys);
303
315
  });
304
316
 
305
317
  provide('mForm', formState);
package/src/submitForm.ts CHANGED
@@ -87,6 +87,11 @@ export interface SubmitFormOptions {
87
87
  */
88
88
  debug?: boolean;
89
89
  typeMatchValid?: boolean;
90
+ /**
91
+ * 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
92
+ * 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
93
+ */
94
+ signal?: AbortSignal;
90
95
  }
91
96
  // #endregion SubmitFormOptions
92
97
 
@@ -127,7 +132,7 @@ interface MountFormInstanceOptions<T> {
127
132
  formProps: Record<string, any>;
128
133
  /** 父级应用上下文,用于继承全局组件、指令、provide 等 */
129
134
  appContext?: AppContext | null;
130
- /** 等待表单初始化的最长时间(毫秒),<=0 表示不注册超时 */
135
+ /** 等待表单初始化的最长时间(毫秒),<=0 时回退到默认超时以保证兜底清理生效 */
131
136
  timeout: number;
132
137
  /** 超时 reject 的错误文案 */
133
138
  timeoutMessage: string;
@@ -135,10 +140,15 @@ interface MountFormInstanceOptions<T> {
135
140
  hidden?: boolean;
136
141
  /** 是否跳过超时注册。调试模式等待人工操作,应传 `true` */
137
142
  skipTimeout?: boolean;
143
+ /** 外部中断信号:abort 时会 reject 并卸载实例、移除容器,用于取消无超时(如 debug)的挂载 */
144
+ signal?: AbortSignal;
138
145
  /** 构造 wrapper 组件 */
139
146
  createWrapper: FormWrapperFactory<T>;
140
147
  }
141
148
 
149
+ /** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
150
+ const DEFAULT_MOUNT_TIMEOUT = 10000;
151
+
142
152
  /**
143
153
  * submitForm / validateForm 的公共脚手架:
144
154
  *
@@ -149,20 +159,36 @@ interface MountFormInstanceOptions<T> {
149
159
  * 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
150
160
  */
151
161
  const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T> => {
152
- const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options;
162
+ const {
163
+ formProps,
164
+ appContext,
165
+ timeout,
166
+ timeoutMessage,
167
+ hidden = true,
168
+ skipTimeout = false,
169
+ signal,
170
+ createWrapper,
171
+ } = options;
153
172
 
154
173
  return new Promise<T>((resolve, reject) => {
155
- const container = document.createElement('div');
156
- if (hidden) {
157
- container.style.display = 'none';
174
+ // 已中断则直接 reject,不创建任何容器/实例
175
+ if (signal?.aborted) {
176
+ reject(signal.reason ?? new Error('mountFormInstance aborted'));
177
+ return;
158
178
  }
159
- document.body.appendChild(container);
160
179
 
161
180
  let cleaned = false;
162
181
  let timer: ReturnType<typeof setTimeout> | null = null;
182
+ let onAbort: (() => void) | null = null;
163
183
  // 用 holder 持有 app,使 cleanup 可在 app 创建之前定义(const app + 无 TDZ / 无 use-before-define)
164
184
  const instance: { app: ReturnType<typeof createApp> | null } = { app: null };
165
185
 
186
+ const container = document.createElement('div');
187
+ if (hidden) {
188
+ container.style.display = 'none';
189
+ }
190
+ document.body.appendChild(container);
191
+
166
192
  const cleanup = () => {
167
193
  if (cleaned) return;
168
194
  cleaned = true;
@@ -170,6 +196,10 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
170
196
  clearTimeout(timer);
171
197
  timer = null;
172
198
  }
199
+ if (signal && onAbort) {
200
+ signal.removeEventListener('abort', onAbort);
201
+ onAbort = null;
202
+ }
173
203
  try {
174
204
  instance.app?.unmount();
175
205
  } catch {
@@ -178,81 +208,101 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
178
208
  container.parentNode?.removeChild(container);
179
209
  };
180
210
 
181
- const formRef = ref<any>(null);
182
-
183
- // extendState 从 formProps 中剥离:不由 Form.vue 的 async watchEffect 异步应用,
184
- // 而是在 wrapper 中通过 sync watch 在 formRef 就绪后直接写入 formState,
185
- // 避免 display 等 filterFunction 在首次渲染时读到 undefined。
186
- // CompareForm / FormPanel 中「formRef.value.formState.services = ...」的做法一致。
187
- const { extendState, ...restFormProps } = formProps;
188
-
189
- const userWrapper = createWrapper({ formRef, formProps: restFormProps, cleanup, resolve, reject });
211
+ // 支持外部通过 AbortSignal 主动中断:debug 模式无超时兜底,若调用方放弃了该 Promise,
212
+ // 可通过 abort 卸载实例、移除遮罩/容器,避免无限驻留在 DOM 中。
213
+ if (signal) {
214
+ onAbort = () => {
215
+ if (cleaned) return;
216
+ reject(signal.reason ?? new Error('mountFormInstance aborted'));
217
+ cleanup();
218
+ };
219
+ signal.addEventListener('abort', onAbort);
220
+ }
190
221
 
191
- const wrapperComponent =
192
- typeof extendState === 'function'
222
+ // 从容器创建到 mount 的全流程统一 try/catch:任一步骤(createWrapper/createApp/上下文合并/mount)
223
+ // 抛错都会走到 cleanup,避免已插入 body 的 container 及未挂载的 app 残留导致泄漏。
224
+ try {
225
+ const formRef = ref<any>(null);
226
+
227
+ // 将 extendState 从 formProps 中剥离:不由 Form.vue 的 async watchEffect 异步应用,
228
+ // 而是在 wrapper 中通过 sync watch 在 formRef 就绪后直接写入 formState,
229
+ // 避免 display 等 filterFunction 在首次渲染时读到 undefined。
230
+ // 与 CompareForm / FormPanel 中「formRef.value.formState.services = ...」的做法一致。
231
+ const { extendState, ...restFormProps } = formProps;
232
+
233
+ const userWrapper = createWrapper({ formRef, formProps: restFormProps, cleanup, resolve, reject });
234
+
235
+ const wrapperComponent =
236
+ typeof extendState === 'function'
237
+ ? defineComponent({
238
+ name: 'MFormExtendStateInjector',
239
+ setup() {
240
+ watch(
241
+ () => formRef.value,
242
+ (form) => {
243
+ if (!form) return;
244
+ let result: any;
245
+ try {
246
+ result = extendState(form.formState);
247
+ } catch (e) {
248
+ console.error('[MForm] extendState failed:', e);
249
+ return;
250
+ }
251
+ // formState 的内置 key 快照:在 extendState 合并前捕获,
252
+ // 供 applyExtendState 禁止 extendState 覆盖这些已有字段(只能新增),
253
+ // 与 Form.vue 中 reservedStateKeys 的语义保持一致。
254
+ const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(form.formState));
255
+ // 合并逻辑收口在 applyExtendState:props 派生的只读 getter 字段
256
+ // (keyProp 等)以普通字段形式返回时会被跳过并告警,避免 proxy set 抛错
257
+ const apply = (state: Record<string, any> | null | undefined) =>
258
+ applyExtendState(form.formState, state, reservedStateKeys);
259
+ if (result && typeof result.then === 'function') {
260
+ result.then(apply, (e: any) => console.error('[MForm] extendState failed:', e));
261
+ } else {
262
+ apply(result);
263
+ }
264
+ },
265
+ { flush: 'sync', immediate: true },
266
+ );
267
+ return () => h(userWrapper);
268
+ },
269
+ })
270
+ : userWrapper;
271
+
272
+ // 静默(隐藏挂载)模式下注入静默标记:vs-code 等重型字段组件可据此跳过自身渲染,
273
+ // 校验/取值依赖 FormItem 与 model 值,与叶子 UI 组件无关(见 FORM_SILENT_MODE_KEY 注释)。
274
+ // 用组件级 provide 而非 app.provide:appContext 合并后 app._context.provides 与父级应用
275
+ // 共享引用,app.provide 会把标记泄漏到父级应用。
276
+ const rootComponent = hidden
193
277
  ? defineComponent({
194
- name: 'MFormExtendStateInjector',
278
+ name: 'MFormSilentProvider',
195
279
  setup() {
196
- watch(
197
- () => formRef.value,
198
- (form) => {
199
- if (!form) return;
200
- let result: any;
201
- try {
202
- result = extendState(form.formState);
203
- } catch (e) {
204
- console.error('[MForm] extendState failed:', e);
205
- return;
206
- }
207
- // 合并逻辑收口在 applyExtendState:props 派生的只读 getter 字段
208
- // (keyProp 等)以普通字段形式返回时会被跳过并告警,避免 proxy set 抛错
209
- const apply = (state: Record<string, any> | null | undefined) =>
210
- applyExtendState(form.formState, state);
211
- if (result && typeof result.then === 'function') {
212
- result.then(apply, (e: any) => console.error('[MForm] extendState failed:', e));
213
- } else {
214
- apply(result);
215
- }
216
- },
217
- { flush: 'sync', immediate: true },
218
- );
219
- return () => h(userWrapper);
280
+ provide(FORM_SILENT_MODE_KEY, true);
281
+ return () => h(wrapperComponent);
220
282
  },
221
283
  })
222
- : userWrapper;
223
-
224
- // 静默(隐藏挂载)模式下注入静默标记:vs-code 等重型字段组件可据此跳过自身渲染,
225
- // 校验/取值依赖 FormItem 与 model 值,与叶子 UI 组件无关(见 FORM_SILENT_MODE_KEY 注释)。
226
- // 用组件级 provide 而非 app.provide:appContext 合并后 app._context.provides 与父级应用
227
- // 共享引用,app.provide 会把标记泄漏到父级应用。
228
- const rootComponent = hidden
229
- ? defineComponent({
230
- name: 'MFormSilentProvider',
231
- setup() {
232
- provide(FORM_SILENT_MODE_KEY, true);
233
- return () => h(wrapperComponent);
234
- },
235
- })
236
- : wrapperComponent;
284
+ : wrapperComponent;
237
285
 
238
- const app = createApp(rootComponent);
239
- instance.app = app;
286
+ const app = createApp(rootComponent);
287
+ instance.app = app;
240
288
 
241
- // 继承父级应用上下文(components / directives / provides / config 等)
242
- if (appContext) {
243
- Object.assign(app._context, appContext);
244
- }
289
+ // 继承父级应用上下文(components / directives / provides / config 等)
290
+ if (appContext) {
291
+ Object.assign(app._context, appContext);
292
+ }
245
293
 
246
- if (timeout > 0 && !skipTimeout) {
247
- timer = setTimeout(() => {
248
- if (!cleaned) {
249
- reject(new Error(timeoutMessage));
250
- cleanup();
251
- }
252
- }, timeout);
253
- }
294
+ // 非 debug(未跳过超时)场景始终注册超时兜底:timeout 为非正数时回退到默认值,
295
+ // 避免表单永不初始化时实例/容器/watcher 无限驻留而泄漏。
296
+ if (!skipTimeout) {
297
+ const effectiveTimeout = timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT;
298
+ timer = setTimeout(() => {
299
+ if (!cleaned) {
300
+ reject(new Error(timeoutMessage));
301
+ cleanup();
302
+ }
303
+ }, effectiveTimeout);
304
+ }
254
305
 
255
- try {
256
306
  app.mount(container);
257
307
  } catch (err) {
258
308
  reject(err);
@@ -450,12 +500,13 @@ const createDebugWrapper = (options: DebugWrapperOptions): Component => {
450
500
  * ```
451
501
  */
452
502
  export const submitForm = (options: SubmitFormOptions): Promise<any> => {
453
- const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, ...formProps } = options;
503
+ const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, signal, ...formProps } = options;
454
504
 
455
505
  return mountFormInstance<any>({
456
506
  formProps,
457
507
  appContext,
458
508
  timeout,
509
+ signal,
459
510
  // 调试模式需把表单展示出来;普通模式隐藏挂载
460
511
  hidden: !debug,
461
512
  // 调试模式等待人工操作,不应用超时
@@ -559,6 +610,11 @@ export interface ValidateFormOptions {
559
610
  */
560
611
  debug?: boolean;
561
612
  typeMatchValid?: boolean;
613
+ /**
614
+ * 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
615
+ * 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
616
+ */
617
+ signal?: AbortSignal;
562
618
  }
563
619
  // #endregion ValidateFormOptions
564
620
 
@@ -654,7 +710,7 @@ export const stripTabItemsLazy = (config: FormConfig): FormConfig => {
654
710
  * ```
655
711
  */
656
712
  export const validateForm = (options: ValidateFormOptions): Promise<string> => {
657
- const { appContext, timeout = 10000, debug = false, config, ...rest } = options;
713
+ const { appContext, timeout = 10000, debug = false, config, signal, ...rest } = options;
658
714
 
659
715
  // 去掉 tab 容器各标签页的 lazy,确保懒加载标签页内的字段也参与校验
660
716
  const formProps = { ...rest, config: stripTabItemsLazy(config) };
@@ -663,6 +719,7 @@ export const validateForm = (options: ValidateFormOptions): Promise<string> => {
663
719
  formProps,
664
720
  appContext,
665
721
  timeout,
722
+ signal,
666
723
  // 调试模式需把表单展示出来;普通模式隐藏挂载
667
724
  hidden: !debug,
668
725
  // 调试模式等待人工操作,不应用超时
@@ -2,7 +2,6 @@
2
2
  .m-fields-group-list-item.tmagic-design-card--flat:last-child {
3
3
  border: 0;
4
4
  }
5
-
6
5
  .el-button--text {
7
6
  padding: 0;
8
7
  margin-bottom: 7px;
@@ -32,9 +31,19 @@
32
31
  }
33
32
 
34
33
  .m-fields-group-list-footer {
35
- display: flex;
36
34
  justify-content: space-between;
37
35
  margin-top: 10px;
38
36
  margin-bottom: 16px;
39
37
  }
40
38
  }
39
+
40
+ /** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
41
+ .m-container-group-list {
42
+ &.outer-gorup_list {
43
+ > .m-fields-group-list {
44
+ > .m-fields-group-list-item {
45
+ margin-bottom: 10px;
46
+ }
47
+ }
48
+ }
49
+ }
@@ -92,4 +92,15 @@
92
92
  text-align: left;
93
93
  }
94
94
  }
95
+
96
+ .m-fields-group-list {
97
+ .el-table__empty-block {
98
+ .el-table__empty-text {
99
+ width: 100%;
100
+ background-color: rgba(0, 0, 0, 0.03);
101
+ margin-top: 8px;
102
+ border-radius: 4px;
103
+ }
104
+ }
105
+ }
95
106
  }
package/src/utils/form.ts CHANGED
@@ -449,16 +449,27 @@ export const sortChange = (data: any[], { prop, order }: SortProp) => {
449
449
  * - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
450
450
  * 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
451
451
  *
452
- * 注意:formState 上由 props 派生的字段(keyProp / popperClass / config / initValues /
453
- * isCompare / lastValues / parentValues)是只读 getter(无 setter),extendState 若以
454
- * 普通字段形式返回同名 key,直接赋值会让 proxy set trap 失败并抛出
455
- * `TypeError: 'set' on proxy: trap returned falsish`,这里统一跳过并告警;
456
- * 如确需覆盖,可在 extendState 中以 get 访问器形式返回。
452
+ * 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
453
+ * 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
454
+ * config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
455
+ * post 等),命中这些 key 时统一跳过并告警。
456
+ *
457
+ * 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
458
+ * 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
459
+ * `TypeError: 'set' on proxy: trap returned falsish`。
457
460
  */
458
- export const applyExtendState = (formState: FormState, state: Record<string, any> | null | undefined): void => {
461
+ export const applyExtendState = (
462
+ formState: FormState,
463
+ state: Record<string, any> | null | undefined,
464
+ reservedKeys?: Set<string | symbol>,
465
+ ): void => {
459
466
  if (!state) return;
460
467
 
461
468
  for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
469
+ if (reservedKeys?.has(key)) {
470
+ continue;
471
+ }
472
+
462
473
  if (!('value' in descriptor)) {
463
474
  descriptor.configurable = true;
464
475
  Object.defineProperty(formState, key, descriptor);
@@ -142,7 +142,7 @@ const stringifyExampleValue = (value: any): string => {
142
142
  };
143
143
 
144
144
  // 参考建议中最多展示的可选值个数,超出以「等」省略。
145
- const MAX_SUGGESTION_OPTIONS = 5;
145
+ const MAX_SUGGESTION_OPTIONS = 20;
146
146
 
147
147
  /**
148
148
  * 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
package/types/index.d.ts CHANGED
@@ -128,6 +128,11 @@ interface SubmitFormOptions {
128
128
  */
129
129
  debug?: boolean;
130
130
  typeMatchValid?: boolean;
131
+ /**
132
+ * 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
133
+ * 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
134
+ */
135
+ signal?: AbortSignal;
131
136
  }
132
137
  /**
133
138
  * 开启 `returnChangeNodes` 时 submitForm 的返回结果
@@ -209,6 +214,11 @@ interface ValidateFormOptions {
209
214
  */
210
215
  debug?: boolean;
211
216
  typeMatchValid?: boolean;
217
+ /**
218
+ * 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
219
+ * 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
220
+ */
221
+ signal?: AbortSignal;
212
222
  }
213
223
  /**
214
224
  * 返回一份去除了 tab 标签页 lazy 的配置副本。
@@ -290,13 +300,16 @@ declare const sortChange: (data: any[], {
290
300
  * - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
291
301
  * 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
292
302
  *
293
- * 注意:formState 上由 props 派生的字段(keyProp / popperClass / config / initValues /
294
- * isCompare / lastValues / parentValues)是只读 getter(无 setter),extendState 若以
295
- * 普通字段形式返回同名 key,直接赋值会让 proxy set trap 失败并抛出
296
- * `TypeError: 'set' on proxy: trap returned falsish`,这里统一跳过并告警;
297
- * 如确需覆盖,可在 extendState 中以 get 访问器形式返回。
303
+ * 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
304
+ * 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
305
+ * config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
306
+ * post 等),命中这些 key 时统一跳过并告警。
307
+ *
308
+ * 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
309
+ * 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
310
+ * `TypeError: 'set' on proxy: trap returned falsish`。
298
311
  */
299
- declare const applyExtendState: (formState: schema_d_exports.FormState, state: Record<string, any> | null | undefined) => void;
312
+ declare const applyExtendState: (formState: schema_d_exports.FormState, state: Record<string, any> | null | undefined, reservedKeys?: Set<string | symbol>) => void;
300
313
  declare const createObjectProp: (prop: string, key: string, name?: string | number) => string;
301
314
  //#endregion
302
315
  //#region temp/packages/form/src/utils/useAddField.d.ts