@tmagic/form 1.8.0-beta.16 → 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.
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,59 +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 reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
58
- const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
59
- if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
60
- else apply(result);
61
- }, {
62
- flush: "sync",
63
- immediate: true
64
- });
65
- return () => h(userWrapper);
66
- }
67
- }) : userWrapper;
68
- const app = createApp(hidden ? defineComponent({
69
- name: "MFormSilentProvider",
70
- setup() {
71
- provide(FORM_SILENT_MODE_KEY, true);
72
- return () => h(wrapperComponent);
73
- }
74
- }) : wrapperComponent);
75
- instance.app = app;
76
- if (appContext) Object.assign(app._context, appContext);
77
- if (timeout > 0 && !skipTimeout) timer = setTimeout(() => {
78
- if (!cleaned) {
79
- reject(new Error(timeoutMessage));
47
+ if (signal) {
48
+ onAbort = () => {
49
+ if (cleaned) return;
50
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
80
51
  cleanup();
81
- }
82
- }, timeout);
52
+ };
53
+ signal.addEventListener("abort", onAbort);
54
+ }
83
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);
84
103
  app.mount(container);
85
104
  } catch (err) {
86
105
  reject(err);
@@ -213,11 +232,12 @@ var createDebugWrapper = (options) => {
213
232
  * ```
214
233
  */
215
234
  var submitForm = (options) => {
216
- const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, ...formProps } = options;
235
+ const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
217
236
  return mountFormInstance({
218
237
  formProps,
219
238
  appContext,
220
239
  timeout,
240
+ signal,
221
241
  hidden: !debug,
222
242
  skipTimeout: debug,
223
243
  timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
@@ -354,7 +374,7 @@ var stripTabItemsLazy = (config) => {
354
374
  * ```
355
375
  */
356
376
  var validateForm = (options) => {
357
- const { appContext, timeout = 1e4, debug = false, config, ...rest } = options;
377
+ const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
358
378
  return mountFormInstance({
359
379
  formProps: {
360
380
  ...rest,
@@ -362,6 +382,7 @@ var validateForm = (options) => {
362
382
  },
363
383
  appContext,
364
384
  timeout,
385
+ signal,
365
386
  hidden: !debug,
366
387
  skipTimeout: debug,
367
388
  timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
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
  }
@@ -4420,6 +4420,8 @@
4420
4420
  });
4421
4421
  //#endregion
4422
4422
  //#region packages/form/src/submitForm.ts
4423
+ /** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
4424
+ var DEFAULT_MOUNT_TIMEOUT = 1e4;
4423
4425
  /**
4424
4426
  * submitForm / validateForm 的公共脚手架:
4425
4427
  *
@@ -4430,14 +4432,19 @@
4430
4432
  * 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
4431
4433
  */
4432
4434
  var mountFormInstance = (options) => {
4433
- const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options;
4435
+ const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
4434
4436
  return new Promise((resolve, reject) => {
4435
- const container = document.createElement("div");
4436
- if (hidden) container.style.display = "none";
4437
- document.body.appendChild(container);
4437
+ if (signal?.aborted) {
4438
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
4439
+ return;
4440
+ }
4438
4441
  let cleaned = false;
4439
4442
  let timer = null;
4443
+ let onAbort = null;
4440
4444
  const instance = { app: null };
4445
+ const container = document.createElement("div");
4446
+ if (hidden) container.style.display = "none";
4447
+ document.body.appendChild(container);
4441
4448
  const cleanup = () => {
4442
4449
  if (cleaned) return;
4443
4450
  cleaned = true;
@@ -4445,59 +4452,71 @@
4445
4452
  clearTimeout(timer);
4446
4453
  timer = null;
4447
4454
  }
4455
+ if (signal && onAbort) {
4456
+ signal.removeEventListener("abort", onAbort);
4457
+ onAbort = null;
4458
+ }
4448
4459
  try {
4449
4460
  instance.app?.unmount();
4450
4461
  } catch {}
4451
4462
  container.parentNode?.removeChild(container);
4452
4463
  };
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));
4464
+ if (signal) {
4465
+ onAbort = () => {
4466
+ if (cleaned) return;
4467
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
4497
4468
  cleanup();
4498
- }
4499
- }, timeout);
4469
+ };
4470
+ signal.addEventListener("abort", onAbort);
4471
+ }
4500
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);
4501
4520
  app.mount(container);
4502
4521
  } catch (err) {
4503
4522
  reject(err);
@@ -4630,11 +4649,12 @@
4630
4649
  * ```
4631
4650
  */
4632
4651
  var submitForm = (options) => {
4633
- const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, ...formProps } = options;
4652
+ const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
4634
4653
  return mountFormInstance({
4635
4654
  formProps,
4636
4655
  appContext,
4637
4656
  timeout,
4657
+ signal,
4638
4658
  hidden: !debug,
4639
4659
  skipTimeout: debug,
4640
4660
  timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
@@ -4771,7 +4791,7 @@
4771
4791
  * ```
4772
4792
  */
4773
4793
  var validateForm = (options) => {
4774
- const { appContext, timeout = 1e4, debug = false, config, ...rest } = options;
4794
+ const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
4775
4795
  return mountFormInstance({
4776
4796
  formProps: {
4777
4797
  ...rest,
@@ -4779,6 +4799,7 @@
4779
4799
  },
4780
4800
  appContext,
4781
4801
  timeout,
4802
+ signal,
4782
4803
  hidden: !debug,
4783
4804
  skipTimeout: debug,
4784
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.16",
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.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.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/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
  // 调试模式等待人工操作,不应用超时
@@ -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/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 的配置副本。