@tmagic/form 1.8.0-beta.16 → 1.8.0-beta.18

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.
@@ -3088,6 +3088,11 @@
3088
3088
  return;
3089
3089
  }
3090
3090
  };
3091
+ /**
3092
+ * 校验取值与字段 type 是否匹配;通过返回 undefined,否则返回错误文案。
3093
+ *
3094
+ * 命中的自定义规则是异步校验器时返回 Promise,内置规则始终同步返回。
3095
+ */
3091
3096
  var validateTypeMatch = (value, mForm, props, message) => {
3092
3097
  if (isEmptyValue(value) || isEmptyArray(value)) return;
3093
3098
  if (!props.config?.name) return;
@@ -3103,34 +3108,106 @@
3103
3108
  });
3104
3109
  return validateBuiltinTypeMatch(value, fieldType, mForm, props, message);
3105
3110
  };
3111
+ var toError = (error) => error instanceof Error ? error : /* @__PURE__ */ new Error(`${error}`);
3106
3112
  var createTypeMatchValidator = (mForm, props, rule) => {
3107
3113
  const originalValidator = typeof rule.validator === "function" ? rule.validator : void 0;
3114
+ /**
3115
+ * 每次校验取一个自增序号,只有最新一轮的结论会被采用。
3116
+ *
3117
+ * 旧值的在途校验不能用自己的结论结算:它可能晚于新校验结束,用过期结论覆盖新结论
3118
+ * (表单取最后结束的那次结果);也不能在新校验开始时无条件按通过结算,否则
3119
+ * `form.validate()` 会对一个还没校验过的值返回成功。因此所有尚未结算的调用都登记在
3120
+ * pendingSettlers 中,等最新一轮出结论后用同一个结论一起结算。
3121
+ *
3122
+ * 不比较取值本身:配了 names 或取值为对象/数组时拿到的是同一个引用,就地修改后比较不出变化。
3123
+ */
3124
+ let generation = 0;
3125
+ let pendingSettlers = [];
3108
3126
  return (asyncValidatorRule, value, callback, source, options) => {
3109
3127
  const actualValue = props.config?.names ? props.model : value;
3110
- try {
3111
- const error = validateTypeMatch(actualValue, mForm, props, rule.message);
3112
- if (error) {
3113
- callback(new Error(error));
3128
+ generation += 1;
3129
+ const currentGeneration = generation;
3130
+ /** 已有更新的一轮校验开始,本轮结论作废 */
3131
+ const isStale = () => currentGeneration !== generation;
3132
+ pendingSettlers.push((args) => callback(...args));
3133
+ /**
3134
+ * 结算本轮与被本轮取代的所有在途校验。
3135
+ *
3136
+ * async-validator 的 callback 不幂等:重复调用会让它的内部计数提前满足、错误信息重复;
3137
+ * 原始 validator 写成 async 时很容易既调 callback 又返回 Promise,故所有回调都收敛到这里,
3138
+ * 由「取出即出队」保证每个调用的 callback 只会被调用一次。
3139
+ */
3140
+ const conclude = (...args) => {
3141
+ if (isStale()) return;
3142
+ const settlers = pendingSettlers;
3143
+ pendingSettlers = [];
3144
+ for (const settle of settlers) settle(args);
3145
+ };
3146
+ /**
3147
+ * 执行原始 validator 并把结果统一转成 callback。
3148
+ *
3149
+ * async-validator 只解析同步返回给它的返回值(true / false / Error / 错误数组 / Promise,
3150
+ * 抛错转成错误信息),异步路径已脱离它的调用栈,这些约定会失效导致校验永不结束,故复刻一份。
3151
+ */
3152
+ const settleWithOriginalValidator = () => {
3153
+ if (isStale()) return;
3154
+ if (!originalValidator) {
3155
+ conclude();
3114
3156
  return;
3115
3157
  }
3116
- } catch (error) {
3117
- console.error(error);
3158
+ let result;
3159
+ try {
3160
+ result = originalValidator({
3161
+ rule: asyncValidatorRule,
3162
+ value: actualValue,
3163
+ callback: conclude,
3164
+ source,
3165
+ options
3166
+ }, {
3167
+ values: mForm?.initValues || {},
3168
+ model: props.model,
3169
+ parent: mForm?.parentValues || {},
3170
+ formValue: mForm?.values || props.model,
3171
+ prop: props.prop,
3172
+ config: props.config
3173
+ }, mForm);
3174
+ } catch (err) {
3175
+ conclude(toError(err));
3176
+ return;
3177
+ }
3178
+ if (isPromise(result)) result.then(() => conclude(), (err) => conclude(toError(err)));
3179
+ else if (result === true) conclude();
3180
+ else if (result === false) {
3181
+ const field = asyncValidatorRule?.fullField || asyncValidatorRule?.field || props.prop;
3182
+ conclude(new Error(rule.message || `${field} fails`));
3183
+ } else if (result instanceof Error || Array.isArray(result)) conclude(result);
3184
+ };
3185
+ const skipFailedTypeMatch = (err) => {
3186
+ console.error(err);
3187
+ settleWithOriginalValidator();
3188
+ };
3189
+ let error;
3190
+ try {
3191
+ error = validateTypeMatch(actualValue, mForm, props, rule.message);
3192
+ } catch (err) {
3193
+ skipFailedTypeMatch(err);
3194
+ return;
3118
3195
  }
3119
- if (originalValidator) return originalValidator({
3120
- rule: asyncValidatorRule,
3121
- value: actualValue,
3122
- callback,
3123
- source,
3124
- options
3125
- }, {
3126
- values: mForm?.initValues || {},
3127
- model: props.model,
3128
- parent: mForm?.parentValues || {},
3129
- formValue: mForm?.values || props.model,
3130
- prop: props.prop,
3131
- config: props.config
3132
- }, mForm);
3133
- callback();
3196
+ if (isPromise(error)) {
3197
+ error.then((asyncMessage) => {
3198
+ if (asyncMessage) {
3199
+ conclude(new Error(asyncMessage));
3200
+ return;
3201
+ }
3202
+ settleWithOriginalValidator();
3203
+ }, skipFailedTypeMatch);
3204
+ return;
3205
+ }
3206
+ if (error) {
3207
+ conclude(new Error(error));
3208
+ return;
3209
+ }
3210
+ settleWithOriginalValidator();
3134
3211
  };
3135
3212
  };
3136
3213
  //#endregion
@@ -3149,15 +3226,16 @@
3149
3226
  const callback = (error) => {
3150
3227
  if (settled) return;
3151
3228
  settled = true;
3152
- if (error) resolve({
3229
+ const first = Array.isArray(error) ? error[0] : error;
3230
+ if (first) resolve({
3153
3231
  result: false,
3154
- message: typeof error === "string" ? error : error.message
3232
+ message: typeof first === "string" ? first : first.message
3155
3233
  });
3156
3234
  else resolve(true);
3157
3235
  };
3158
3236
  try {
3159
3237
  const result = validator(void 0, value, callback);
3160
- if (result !== null && typeof result.then === "function") Promise.resolve(result).then(() => {
3238
+ if (result && typeof result.then === "function") Promise.resolve(result).then(() => {
3161
3239
  if (!settled) callback();
3162
3240
  }, (err) => {
3163
3241
  callback(err instanceof Error ? err : new Error(String(err)));
@@ -4420,6 +4498,8 @@
4420
4498
  });
4421
4499
  //#endregion
4422
4500
  //#region packages/form/src/submitForm.ts
4501
+ /** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
4502
+ var DEFAULT_MOUNT_TIMEOUT = 1e4;
4423
4503
  /**
4424
4504
  * submitForm / validateForm 的公共脚手架:
4425
4505
  *
@@ -4430,14 +4510,19 @@
4430
4510
  * 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
4431
4511
  */
4432
4512
  var mountFormInstance = (options) => {
4433
- const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options;
4513
+ const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
4434
4514
  return new Promise((resolve, reject) => {
4435
- const container = document.createElement("div");
4436
- if (hidden) container.style.display = "none";
4437
- document.body.appendChild(container);
4515
+ if (signal?.aborted) {
4516
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
4517
+ return;
4518
+ }
4438
4519
  let cleaned = false;
4439
4520
  let timer = null;
4521
+ let onAbort = null;
4440
4522
  const instance = { app: null };
4523
+ const container = document.createElement("div");
4524
+ if (hidden) container.style.display = "none";
4525
+ document.body.appendChild(container);
4441
4526
  const cleanup = () => {
4442
4527
  if (cleaned) return;
4443
4528
  cleaned = true;
@@ -4445,59 +4530,71 @@
4445
4530
  clearTimeout(timer);
4446
4531
  timer = null;
4447
4532
  }
4533
+ if (signal && onAbort) {
4534
+ signal.removeEventListener("abort", onAbort);
4535
+ onAbort = null;
4536
+ }
4448
4537
  try {
4449
4538
  instance.app?.unmount();
4450
4539
  } catch {}
4451
4540
  container.parentNode?.removeChild(container);
4452
4541
  };
4453
- const formRef = (0, vue.ref)(null);
4454
- const { extendState, ...restFormProps } = formProps;
4455
- const userWrapper = createWrapper({
4456
- formRef,
4457
- formProps: restFormProps,
4458
- cleanup,
4459
- resolve,
4460
- reject
4461
- });
4462
- const wrapperComponent = typeof extendState === "function" ? (0, vue.defineComponent)({
4463
- name: "MFormExtendStateInjector",
4464
- setup() {
4465
- (0, vue.watch)(() => formRef.value, (form) => {
4466
- if (!form) return;
4467
- let result;
4468
- try {
4469
- result = extendState(form.formState);
4470
- } catch (e) {
4471
- console.error("[MForm] extendState failed:", e);
4472
- return;
4473
- }
4474
- const reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
4475
- const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
4476
- if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
4477
- else apply(result);
4478
- }, {
4479
- flush: "sync",
4480
- immediate: true
4481
- });
4482
- return () => (0, vue.h)(userWrapper);
4483
- }
4484
- }) : userWrapper;
4485
- const app = (0, vue.createApp)(hidden ? (0, vue.defineComponent)({
4486
- name: "MFormSilentProvider",
4487
- setup() {
4488
- (0, vue.provide)(FORM_SILENT_MODE_KEY, true);
4489
- return () => (0, vue.h)(wrapperComponent);
4490
- }
4491
- }) : wrapperComponent);
4492
- instance.app = app;
4493
- if (appContext) Object.assign(app._context, appContext);
4494
- if (timeout > 0 && !skipTimeout) timer = setTimeout(() => {
4495
- if (!cleaned) {
4496
- reject(new Error(timeoutMessage));
4542
+ if (signal) {
4543
+ onAbort = () => {
4544
+ if (cleaned) return;
4545
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
4497
4546
  cleanup();
4498
- }
4499
- }, timeout);
4547
+ };
4548
+ signal.addEventListener("abort", onAbort);
4549
+ }
4500
4550
  try {
4551
+ const formRef = (0, vue.ref)(null);
4552
+ const { extendState, ...restFormProps } = formProps;
4553
+ const userWrapper = createWrapper({
4554
+ formRef,
4555
+ formProps: restFormProps,
4556
+ cleanup,
4557
+ resolve,
4558
+ reject
4559
+ });
4560
+ const wrapperComponent = typeof extendState === "function" ? (0, vue.defineComponent)({
4561
+ name: "MFormExtendStateInjector",
4562
+ setup() {
4563
+ (0, vue.watch)(() => formRef.value, (form) => {
4564
+ if (!form) return;
4565
+ let result;
4566
+ try {
4567
+ result = extendState(form.formState);
4568
+ } catch (e) {
4569
+ console.error("[MForm] extendState failed:", e);
4570
+ return;
4571
+ }
4572
+ const reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
4573
+ const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
4574
+ if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
4575
+ else apply(result);
4576
+ }, {
4577
+ flush: "sync",
4578
+ immediate: true
4579
+ });
4580
+ return () => (0, vue.h)(userWrapper);
4581
+ }
4582
+ }) : userWrapper;
4583
+ const app = (0, vue.createApp)(hidden ? (0, vue.defineComponent)({
4584
+ name: "MFormSilentProvider",
4585
+ setup() {
4586
+ (0, vue.provide)(FORM_SILENT_MODE_KEY, true);
4587
+ return () => (0, vue.h)(wrapperComponent);
4588
+ }
4589
+ }) : wrapperComponent);
4590
+ instance.app = app;
4591
+ if (appContext) Object.assign(app._context, appContext);
4592
+ if (!skipTimeout) timer = setTimeout(() => {
4593
+ if (!cleaned) {
4594
+ reject(new Error(timeoutMessage));
4595
+ cleanup();
4596
+ }
4597
+ }, timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT);
4501
4598
  app.mount(container);
4502
4599
  } catch (err) {
4503
4600
  reject(err);
@@ -4630,11 +4727,12 @@
4630
4727
  * ```
4631
4728
  */
4632
4729
  var submitForm = (options) => {
4633
- const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, ...formProps } = options;
4730
+ const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
4634
4731
  return mountFormInstance({
4635
4732
  formProps,
4636
4733
  appContext,
4637
4734
  timeout,
4735
+ signal,
4638
4736
  hidden: !debug,
4639
4737
  skipTimeout: debug,
4640
4738
  timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
@@ -4771,7 +4869,7 @@
4771
4869
  * ```
4772
4870
  */
4773
4871
  var validateForm = (options) => {
4774
- const { appContext, timeout = 1e4, debug = false, config, ...rest } = options;
4872
+ const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
4775
4873
  return mountFormInstance({
4776
4874
  formProps: {
4777
4875
  ...rest,
@@ -4779,6 +4877,7 @@
4779
4877
  },
4780
4878
  appContext,
4781
4879
  timeout,
4880
+ signal,
4782
4881
  hidden: !debug,
4783
4882
  skipTimeout: debug,
4784
4883
  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.16",
2
+ "version": "1.8.0-beta.18",
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.16",
56
- "@tmagic/utils": "1.8.0-beta.16",
57
- "@tmagic/form-schema": "1.8.0-beta.16"
55
+ "@tmagic/design": "1.8.0-beta.18",
56
+ "@tmagic/utils": "1.8.0-beta.18",
57
+ "@tmagic/form-schema": "1.8.0-beta.18"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "typescript": {
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,85 +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
- // formState 的内置 key 快照:在 extendState 合并前捕获,
208
- // 供 applyExtendState 禁止 extendState 覆盖这些已有字段(只能新增),
209
- // 与 Form.vue 中 reservedStateKeys 的语义保持一致。
210
- const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(form.formState));
211
- // 合并逻辑收口在 applyExtendState:props 派生的只读 getter 字段
212
- // (keyProp 等)以普通字段形式返回时会被跳过并告警,避免 proxy set 抛错
213
- const apply = (state: Record<string, any> | null | undefined) =>
214
- applyExtendState(form.formState, state, reservedStateKeys);
215
- if (result && typeof result.then === 'function') {
216
- result.then(apply, (e: any) => console.error('[MForm] extendState failed:', e));
217
- } else {
218
- apply(result);
219
- }
220
- },
221
- { flush: 'sync', immediate: true },
222
- );
223
- return () => h(userWrapper);
280
+ provide(FORM_SILENT_MODE_KEY, true);
281
+ return () => h(wrapperComponent);
224
282
  },
225
283
  })
226
- : userWrapper;
227
-
228
- // 静默(隐藏挂载)模式下注入静默标记:vs-code 等重型字段组件可据此跳过自身渲染,
229
- // 校验/取值依赖 FormItem 与 model 值,与叶子 UI 组件无关(见 FORM_SILENT_MODE_KEY 注释)。
230
- // 用组件级 provide 而非 app.provide:appContext 合并后 app._context.provides 与父级应用
231
- // 共享引用,app.provide 会把标记泄漏到父级应用。
232
- const rootComponent = hidden
233
- ? defineComponent({
234
- name: 'MFormSilentProvider',
235
- setup() {
236
- provide(FORM_SILENT_MODE_KEY, true);
237
- return () => h(wrapperComponent);
238
- },
239
- })
240
- : wrapperComponent;
284
+ : wrapperComponent;
241
285
 
242
- const app = createApp(rootComponent);
243
- instance.app = app;
286
+ const app = createApp(rootComponent);
287
+ instance.app = app;
244
288
 
245
- // 继承父级应用上下文(components / directives / provides / config 等)
246
- if (appContext) {
247
- Object.assign(app._context, appContext);
248
- }
289
+ // 继承父级应用上下文(components / directives / provides / config 等)
290
+ if (appContext) {
291
+ Object.assign(app._context, appContext);
292
+ }
249
293
 
250
- if (timeout > 0 && !skipTimeout) {
251
- timer = setTimeout(() => {
252
- if (!cleaned) {
253
- reject(new Error(timeoutMessage));
254
- cleanup();
255
- }
256
- }, timeout);
257
- }
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
+ }
258
305
 
259
- try {
260
306
  app.mount(container);
261
307
  } catch (err) {
262
308
  reject(err);
@@ -454,12 +500,13 @@ const createDebugWrapper = (options: DebugWrapperOptions): Component => {
454
500
  * ```
455
501
  */
456
502
  export const submitForm = (options: SubmitFormOptions): Promise<any> => {
457
- const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, ...formProps } = options;
503
+ const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, signal, ...formProps } = options;
458
504
 
459
505
  return mountFormInstance<any>({
460
506
  formProps,
461
507
  appContext,
462
508
  timeout,
509
+ signal,
463
510
  // 调试模式需把表单展示出来;普通模式隐藏挂载
464
511
  hidden: !debug,
465
512
  // 调试模式等待人工操作,不应用超时
@@ -563,6 +610,11 @@ export interface ValidateFormOptions {
563
610
  */
564
611
  debug?: boolean;
565
612
  typeMatchValid?: boolean;
613
+ /**
614
+ * 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
615
+ * 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
616
+ */
617
+ signal?: AbortSignal;
566
618
  }
567
619
  // #endregion ValidateFormOptions
568
620
 
@@ -658,7 +710,7 @@ export const stripTabItemsLazy = (config: FormConfig): FormConfig => {
658
710
  * ```
659
711
  */
660
712
  export const validateForm = (options: ValidateFormOptions): Promise<string> => {
661
- const { appContext, timeout = 10000, debug = false, config, ...rest } = options;
713
+ const { appContext, timeout = 10000, debug = false, config, signal, ...rest } = options;
662
714
 
663
715
  // 去掉 tab 容器各标签页的 lazy,确保懒加载标签页内的字段也参与校验
664
716
  const formProps = { ...rest, config: stripTabItemsLazy(config) };
@@ -667,6 +719,7 @@ export const validateForm = (options: ValidateFormOptions): Promise<string> => {
667
719
  formProps,
668
720
  appContext,
669
721
  timeout,
722
+ signal,
670
723
  // 调试模式需把表单展示出来;普通模式隐藏挂载
671
724
  hidden: !debug,
672
725
  // 调试模式等待人工操作,不应用超时