@tmagic/form 1.8.0-beta.25 → 1.8.0-beta.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/es/Form.vue_vue_type_script_setup_true_lang.js +47 -120
  2. package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +3 -3
  3. package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +3 -3
  4. package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +3 -3
  5. package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +3 -0
  6. package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +36 -51
  7. package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +6 -0
  8. package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +4 -1
  9. package/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +37 -15
  10. package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +109 -97
  11. package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +7 -2
  12. package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +3 -0
  13. package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +4 -1
  14. package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +7 -0
  15. package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +7 -5
  16. package/dist/es/containers/table/useTableColumns.js +5 -13
  17. package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +71 -78
  18. package/dist/es/containers/table-group-list/useAdd.js +26 -16
  19. package/dist/es/containers/table-group-list/useScrollLastItemIntoView.js +72 -0
  20. package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +1 -1
  21. package/dist/es/fields/CheckboxGroup/Index.js +5 -0
  22. package/dist/es/fields/{CheckboxGroup.vue_vue_type_script_setup_true_lang.js → CheckboxGroup/Index.vue_vue_type_script_setup_true_lang.js} +6 -7
  23. package/dist/es/fields/CheckboxGroup/effect.js +8 -0
  24. package/dist/es/fields/Date/Index.js +5 -0
  25. package/dist/es/fields/{Date.vue_vue_type_script_setup_true_lang.js → Date/Index.vue_vue_type_script_setup_true_lang.js} +5 -7
  26. package/dist/es/fields/Date/effect.js +10 -0
  27. package/dist/es/fields/DateTime/Index.js +5 -0
  28. package/dist/es/fields/{DateTime.vue_vue_type_script_setup_true_lang.js → DateTime/Index.vue_vue_type_script_setup_true_lang.js} +5 -9
  29. package/dist/es/fields/DateTime/effect.js +15 -0
  30. package/dist/es/fields/Display/Index.js +5 -0
  31. package/dist/es/fields/{Display.vue_vue_type_script_setup_true_lang.js → Display/Index.vue_vue_type_script_setup_true_lang.js} +6 -7
  32. package/dist/es/fields/Display/effect.js +8 -0
  33. package/dist/es/fields/DynamicField/Index.js +5 -0
  34. package/dist/es/fields/{DynamicField.vue_vue_type_script_setup_true_lang.js → DynamicField/Index.vue_vue_type_script_setup_true_lang.js} +11 -15
  35. package/dist/es/fields/DynamicField/effect.js +40 -0
  36. package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +1 -1
  37. package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +1 -1
  38. package/dist/es/fields/NumberRange/Index.js +5 -0
  39. package/dist/es/fields/{NumberRange.vue_vue_type_script_setup_true_lang.js → NumberRange/Index.vue_vue_type_script_setup_true_lang.js} +5 -6
  40. package/dist/es/fields/NumberRange/effect.js +8 -0
  41. package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +13 -6
  42. package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +1 -1
  43. package/dist/es/headless.js +13 -0
  44. package/dist/es/index.js +17 -12
  45. package/dist/es/plugin.js +57 -41
  46. package/dist/es/schema.js +6 -8
  47. package/dist/es/style.css +81 -1
  48. package/dist/es/submitForm.js +44 -194
  49. package/dist/es/utils/builtInFields.js +49 -0
  50. package/dist/es/utils/collectFields.js +407 -0
  51. package/dist/es/utils/config.js +1 -8
  52. package/dist/es/utils/fieldInnerConfig.js +42 -0
  53. package/dist/es/utils/fieldValueEffects.js +80 -0
  54. package/dist/es/utils/form.js +96 -43
  55. package/dist/es/utils/formStateProxy.js +111 -0
  56. package/dist/es/utils/registerField.js +158 -0
  57. package/dist/es/utils/submitHeadless.js +54 -0
  58. package/dist/es/utils/tableGroupList.js +85 -0
  59. package/dist/es/utils/typeMatch.js +17 -19
  60. package/dist/es/utils/validateError.js +57 -0
  61. package/dist/es/utils/validateValues.js +115 -0
  62. package/dist/style.css +81 -1
  63. package/dist/themes/magic-admin.css +83 -7
  64. package/dist/tmagic-form-headless.umd.cjs +4080 -0
  65. package/dist/tmagic-form.umd.cjs +2020 -876
  66. package/package.json +10 -4
  67. package/src/Form.vue +49 -149
  68. package/src/FormBox.vue +5 -4
  69. package/src/FormDialog.vue +5 -5
  70. package/src/FormDrawer.vue +5 -5
  71. package/src/containers/Col.vue +2 -0
  72. package/src/containers/Container.vue +86 -157
  73. package/src/containers/Fieldset.vue +5 -0
  74. package/src/containers/FlexLayout.vue +2 -0
  75. package/src/containers/GroupList.vue +41 -11
  76. package/src/containers/GroupListItem.vue +83 -80
  77. package/src/containers/Panel.vue +3 -0
  78. package/src/containers/Row.vue +2 -0
  79. package/src/containers/Step.vue +2 -0
  80. package/src/containers/Tabs.vue +13 -0
  81. package/src/containers/table/ActionsColumn.vue +1 -1
  82. package/src/containers/table/Table.vue +4 -3
  83. package/src/containers/table/usePagination.ts +1 -1
  84. package/src/containers/table/useSortable.ts +1 -1
  85. package/src/containers/table/useTableColumns.ts +9 -19
  86. package/src/containers/table-group-list/TableGroupList.vue +43 -71
  87. package/src/containers/table-group-list/useAdd.ts +40 -25
  88. package/src/containers/table-group-list/useScrollLastItemIntoView.ts +94 -0
  89. package/src/fields/{CheckboxGroup.vue → CheckboxGroup/Index.vue} +3 -8
  90. package/src/fields/CheckboxGroup/effect.ts +27 -0
  91. package/src/fields/{Date.vue → Date/Index.vue} +2 -5
  92. package/src/fields/Date/effect.ts +29 -0
  93. package/src/fields/{DateTime.vue → DateTime/Index.vue} +2 -16
  94. package/src/fields/DateTime/effect.ts +36 -0
  95. package/src/fields/{Display.vue → Display/Index.vue} +3 -7
  96. package/src/fields/Display/effect.ts +28 -0
  97. package/src/fields/{DynamicField.vue → DynamicField/Index.vue} +11 -11
  98. package/src/fields/DynamicField/effect.ts +74 -0
  99. package/src/fields/{NumberRange.vue → NumberRange/Index.vue} +2 -6
  100. package/src/fields/NumberRange/effect.ts +27 -0
  101. package/src/fields/Select.vue +13 -4
  102. package/src/headless.ts +78 -0
  103. package/src/index.ts +32 -15
  104. package/src/plugin.ts +63 -42
  105. package/src/schema.ts +6 -8
  106. package/src/submitForm.ts +81 -409
  107. package/src/theme/group-list.scss +107 -1
  108. package/src/theme/themes/magic-admin/index.scss +2 -8
  109. package/src/utils/builtInFields.ts +69 -0
  110. package/src/utils/collectFields.ts +561 -0
  111. package/src/utils/config.ts +1 -15
  112. package/src/utils/fieldInnerConfig.ts +127 -0
  113. package/src/utils/fieldValueEffects.ts +140 -0
  114. package/src/utils/form.ts +141 -75
  115. package/src/utils/formStateProxy.ts +149 -0
  116. package/src/utils/registerField.ts +308 -0
  117. package/src/utils/submitHeadless.ts +206 -0
  118. package/src/utils/tableGroupList.ts +120 -0
  119. package/src/utils/typeMatch.ts +19 -15
  120. package/src/utils/validateError.ts +92 -0
  121. package/src/utils/validateValues.ts +177 -0
  122. package/types/headless.d.ts +682 -0
  123. package/types/index.d.ts +631 -272
  124. package/dist/es/fields/CheckboxGroup.js +0 -5
  125. package/dist/es/fields/Date.js +0 -5
  126. package/dist/es/fields/DateTime.js +0 -5
  127. package/dist/es/fields/Display.js +0 -5
  128. package/dist/es/fields/DynamicField.js +0 -5
  129. package/dist/es/fields/NumberRange.js +0 -5
  130. package/dist/es/utils/silentLeafFieldTypes.js +0 -49
  131. package/src/utils/silentLeafFieldTypes.ts +0 -71
package/src/submitForm.ts CHANGED
@@ -16,102 +16,25 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
+ import { type AppContext, type Component, createApp, defineComponent, h, nextTick, type Ref, ref } from 'vue';
20
+
19
21
  import {
20
- type AppContext,
21
- type Component,
22
- createApp,
23
- defineComponent,
24
- h,
25
- nextTick,
26
- provide,
27
- type Ref,
28
- ref,
29
- watch,
30
- } from 'vue';
31
-
32
- import { applyExtendState } from './utils/form';
22
+ submitForm as submitFormHeadless,
23
+ type SubmitFormOptions,
24
+ type SubmitFormResult,
25
+ validateForm as validateFormHeadless,
26
+ type ValidateFormOptions,
27
+ } from './utils/submitHeadless';
33
28
  import Form from './Form.vue';
34
- import { type ChangeRecord, FORM_SILENT_MODE_KEY, type FormConfig, type FormState } from './schema';
29
+ import { type ChangeRecord, type FormConfig } from './schema';
35
30
 
36
- // #region SubmitFormOptions
37
- /**
38
- * submitForm 函数参数(与 Form.vue 组件 props 对齐)
39
- */
40
- export interface SubmitFormOptions {
41
- /** 表单配置 */
42
- config: FormConfig;
43
- /** 表单初始值 */
44
- initValues?: Record<string, any>;
45
- /** 需对比的值(开启对比模式时传入) */
46
- lastValues?: Record<string, any>;
47
- /** 是否开启对比模式 */
48
- isCompare?: boolean;
49
- parentValues?: Record<string, any>;
50
- labelWidth?: string;
51
- disabled?: boolean;
52
- height?: string;
53
- stepActive?: string | number;
54
- size?: 'small' | 'default' | 'large';
55
- inline?: boolean;
56
- labelPosition?: string;
57
- keyProp?: string;
58
- popperClass?: string;
59
- preventSubmitDefault?: boolean;
60
- /**
61
- * 表单校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
62
- * 默认 `true`,置为 `false` 时直接使用字段 name。
63
- */
64
- useFieldTextInError?: boolean;
65
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
66
- /** 透传给 Form.submitForm 的参数:是否直接返回原始响应式 values */
67
- native?: boolean;
68
- /**
69
- * 是否在 resolve 结果中携带 changeRecords(变更记录)。
70
- * 开启后 resolve 的结果为 `{ values, changeRecords }`,否则仅 resolve values。
71
- */
72
- returnChangeRecords?: boolean;
73
- /**
74
- * 父级应用上下文,用于继承全局组件、指令、provide 等。
75
- * 通常通过 `app._context` 或 `getCurrentInstance()?.appContext` 获取。
76
- */
77
- appContext?: AppContext | null;
78
- /** 等待表单初始化的最长时间(毫秒),超时将以错误 reject。默认 10000ms */
79
- timeout?: number;
80
- /**
81
- * 调试模式。默认 `false`。
82
- *
83
- * - `false`:表单以隐藏方式挂载,初始化完成后自动提交(原有行为)。
84
- * - `true`:将表单以弹层形式可见地渲染在页面上,需手动点击「确定」才会触发校验/提交,
85
- * 点击「取消」则以 reject 中断;校验失败时保留弹层并展示错误信息,便于修正后重试。
86
- * 调试模式下 `timeout` 不生效(等待人工操作)。
87
- */
88
- debug?: boolean;
89
- typeMatchValid?: boolean;
90
- /**
91
- * 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
92
- * 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
93
- */
94
- signal?: AbortSignal;
95
- }
96
- // #endregion SubmitFormOptions
97
-
98
- // #region SubmitFormResult
99
- /**
100
- * 开启 `returnChangeNodes` 时 submitForm 的返回结果
101
- */
102
- export interface SubmitFormResult {
103
- /** 校验通过后的表单值 */
104
- values: any;
105
- /** 表单变更记录 */
106
- changeRecords: ChangeRecord[];
107
- }
108
- // #endregion SubmitFormResult
31
+ export type { SubmitFormOptions, SubmitFormResult, ValidateFormOptions };
109
32
 
110
33
  // #region mountFormInstance
111
34
  /**
112
35
  * 构造 wrapper 组件的工厂:在合适时机调用 MForm 实例方法并 resolve/reject、清理实例。
113
36
  *
114
- * 由调用方决定「等待 `initialized` 后自动执行」还是「等待人工触发」(debug 模式),
37
+ * 由调用方决定「等待 `initialized` 后自动执行」还是「等待人工触发」(`dialog: true`),
115
38
  * 以及「调用 `submitForm` 还是 `validate`」「结果如何包装」,从而复用公共脚手架。
116
39
  */
117
40
  type FormWrapperFactory<T> = (ctx: {
@@ -132,43 +55,22 @@ interface MountFormInstanceOptions<T> {
132
55
  formProps: Record<string, any>;
133
56
  /** 父级应用上下文,用于继承全局组件、指令、provide 等 */
134
57
  appContext?: AppContext | null;
135
- /** 等待表单初始化的最长时间(毫秒),<=0 时回退到默认超时以保证兜底清理生效 */
136
- timeout: number;
137
- /** 超时 reject 的错误文案 */
138
- timeoutMessage: string;
139
- /** 是否以 `display:none` 隐藏容器。调试模式需可见,应传 `false` */
140
- hidden?: boolean;
141
- /** 是否跳过超时注册。调试模式等待人工操作,应传 `true` */
142
- skipTimeout?: boolean;
143
- /** 外部中断信号:abort 时会 reject 并卸载实例、移除容器,用于取消无超时(如 debug)的挂载 */
58
+ /** 外部中断信号:abort 时会 reject 并卸载实例、移除容器,用于取消等待人工操作的挂载 */
144
59
  signal?: AbortSignal;
145
60
  /** 构造 wrapper 组件 */
146
61
  createWrapper: FormWrapperFactory<T>;
147
62
  }
148
63
 
149
- /** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
150
- const DEFAULT_MOUNT_TIMEOUT = 10000;
151
-
152
64
  /**
153
- * submitForm / validateForm 的公共脚手架:
65
+ * `dialog: true` 时 submitForm / validateForm 的公共脚手架:
154
66
  *
155
- * 创建临时容器 → 挂载一个 wrapper 组件(内含 MForm)→ 提供统一的 cleanup / timeout / appContext 继承 →
67
+ * 创建临时容器 → 挂载一个 wrapper 组件(内含 MForm)→ 提供统一的 cleanup / appContext 继承 →
156
68
  * 由 `createWrapper` 决定「何时调用 MForm 的哪个方法、如何 resolve/reject」。
157
69
  *
158
- * 调用方只需关注差异逻辑(调用 `submitForm` 还是 `validate`、结果如何包装、是否需要调试弹层),
159
- * 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
70
+ * 挂载出来的表单等待人工点击,因此没有超时兜底,靠「确定」/「取消」/`signal` 结束。
160
71
  */
161
72
  const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T> => {
162
- const {
163
- formProps,
164
- appContext,
165
- timeout,
166
- timeoutMessage,
167
- hidden = true,
168
- skipTimeout = false,
169
- signal,
170
- createWrapper,
171
- } = options;
73
+ const { formProps, appContext, signal, createWrapper } = options;
172
74
 
173
75
  return new Promise<T>((resolve, reject) => {
174
76
  // 已中断则直接 reject,不创建任何容器/实例
@@ -178,24 +80,16 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
178
80
  }
179
81
 
180
82
  let cleaned = false;
181
- let timer: ReturnType<typeof setTimeout> | null = null;
182
83
  let onAbort: (() => void) | null = null;
183
84
  // 用 holder 持有 app,使 cleanup 可在 app 创建之前定义(const app + 无 TDZ / 无 use-before-define)
184
85
  const instance: { app: ReturnType<typeof createApp> | null } = { app: null };
185
86
 
186
87
  const container = document.createElement('div');
187
- if (hidden) {
188
- container.style.display = 'none';
189
- }
190
88
  document.body.appendChild(container);
191
89
 
192
90
  const cleanup = () => {
193
91
  if (cleaned) return;
194
92
  cleaned = true;
195
- if (timer) {
196
- clearTimeout(timer);
197
- timer = null;
198
- }
199
93
  if (signal && onAbort) {
200
94
  signal.removeEventListener('abort', onAbort);
201
95
  onAbort = null;
@@ -208,7 +102,7 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
208
102
  container.parentNode?.removeChild(container);
209
103
  };
210
104
 
211
- // 支持外部通过 AbortSignal 主动中断:debug 模式无超时兜底,若调用方放弃了该 Promise,
105
+ // 支持外部通过 AbortSignal 主动中断:弹层无超时兜底,若调用方放弃了该 Promise,
212
106
  // 可通过 abort 卸载实例、移除遮罩/容器,避免无限驻留在 DOM 中。
213
107
  if (signal) {
214
108
  onAbort = () => {
@@ -224,66 +118,9 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
224
118
  try {
225
119
  const formRef = ref<any>(null);
226
120
 
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
277
- ? defineComponent({
278
- name: 'MFormSilentProvider',
279
- setup() {
280
- provide(FORM_SILENT_MODE_KEY, true);
281
- return () => h(wrapperComponent);
282
- },
283
- })
284
- : wrapperComponent;
121
+ const wrapperComponent = createWrapper({ formRef, formProps, cleanup, resolve, reject });
285
122
 
286
- const app = createApp(rootComponent);
123
+ const app = createApp(wrapperComponent);
287
124
  instance.app = app;
288
125
 
289
126
  // 继承父级应用上下文(components / directives / provides / config 等)
@@ -291,18 +128,6 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
291
128
  Object.assign(app._context, appContext);
292
129
  }
293
130
 
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
- }
305
-
306
131
  app.mount(container);
307
132
  } catch (err) {
308
133
  reject(err);
@@ -312,8 +137,8 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
312
137
  };
313
138
  // #endregion mountFormInstance
314
139
 
315
- // #region createDebugWrapper
316
- interface DebugWrapperOptions {
140
+ // #region createDialogWrapper
141
+ interface DialogWrapperOptions {
317
142
  /** 指向挂载的 MForm 实例 */
318
143
  formRef: Ref<any>;
319
144
  /** 透传给 Form 组件的 props */
@@ -332,12 +157,12 @@ interface DebugWrapperOptions {
332
157
  }
333
158
 
334
159
  /**
335
- * 构造调试模式下的可见弹层 wrapper:以 fixed 遮罩居中渲染 MForm,提供「确定 / 取消」按钮与错误展示区。
160
+ * 构造可见弹层 wrapper:以 fixed 遮罩居中渲染 MForm,提供「确定 / 取消」按钮与错误展示区。
336
161
  *
337
- * submitForm 与 validateForm 的调试弹层 UI 完全一致,仅「确定」时调用的实例方法、错误处理与取消文案不同,
162
+ * submitForm 与 validateForm 的弹层 UI 完全一致,仅「确定」时调用的实例方法、错误处理与取消文案不同,
338
163
  * 这些差异通过 `onConfirm` / `onCancel` 注入,弹层结构在此统一收口。
339
164
  */
340
- const createDebugWrapper = (options: DebugWrapperOptions): Component => {
165
+ const createDialogWrapper = (options: DialogWrapperOptions): Component => {
341
166
  const { formRef, formProps, title, name, onConfirm, onCancel } = options;
342
167
 
343
168
  const btnBase = {
@@ -461,63 +286,22 @@ const createDebugWrapper = (options: DebugWrapperOptions): Component => {
461
286
  },
462
287
  });
463
288
  };
464
- // #endregion createDebugWrapper
289
+ // #endregion createDialogWrapper
465
290
 
466
291
  /**
467
- * 以命令式方式调用 Form.vue 完成一次表单校验/提交。
468
- *
469
- * 类似 ElMessage 的用法:传入 props(包含 `config`/`initValues` 等),函数内部会临时挂载
470
- * 一个不可见的 Form 组件实例,等待初始化完成后调用其 `submitForm` 方法,
471
- * 校验通过则 resolve 表单值,校验失败则 reject 错误信息,最后自动卸载实例。
472
- *
473
- * @example
474
- * ```ts
475
- * import { submitForm } from '@tmagic/form';
476
- *
477
- * try {
478
- * const values = await submitForm({
479
- * config: [...],
480
- * initValues: { name: 'foo' },
481
- * });
482
- * console.log(values);
483
- * } catch (e) {
484
- * console.error(e);
485
- * }
292
+ * 可见弹层提交:把表单渲染在弹层里,点击「确定」才调用挂载实例的 `submitForm`。
486
293
  *
487
- * // 需要同时获取变更记录时:
488
- * const { values, changeRecords } = await submitForm({
489
- * config: [...],
490
- * initValues: { name: 'foo' },
491
- * returnChangeRecords: true,
492
- * });
493
- *
494
- * // 调试模式:可见地渲染表单,点击「确定」才提交:
495
- * const values = await submitForm({
496
- * config: [...],
497
- * initValues: { name: 'foo' },
498
- * debug: true,
499
- * });
500
- * ```
294
+ * 用于需要用户填写/确认的场景;默认路径仍是无渲染校验(见 `submitForm` 的说明)。
501
295
  */
502
- export const submitForm = (options: SubmitFormOptions): Promise<any> => {
503
- const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, signal, ...formProps } = options;
296
+ const submitFormByDialogRender = (options: SubmitFormOptions): Promise<any> => {
297
+ const { native, appContext, returnChangeRecords, signal, dialog, title, ...formProps } = options;
504
298
 
505
299
  return mountFormInstance<any>({
506
300
  formProps,
507
301
  appContext,
508
- timeout,
509
302
  signal,
510
- // 调试模式需把表单展示出来;普通模式隐藏挂载
511
- hidden: !debug,
512
- // 调试模式等待人工操作,不应用超时
513
- skipTimeout: debug,
514
- timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
515
303
  createWrapper: ({ formRef, formProps, cleanup, resolve, reject }) => {
516
- /**
517
- * 执行一次提交:nextTick 等待子组件渲染 → 快照 changeRecords → 调用实例 submitForm → resolve。
518
- * 校验失败时交给 `onValidateError` 决定「保留弹层展示错误(debug)」还是「reject 并清理(普通)」,
519
- * 从而让 debug 与普通模式共享同一份提交逻辑。
520
- */
304
+ // 执行一次提交:nextTick 等待子组件渲染 → 快照 changeRecords → 调用实例 submitForm → resolve
521
305
  const doSubmit = async (onValidateError: (err: any) => void) => {
522
306
  try {
523
307
  // 等待子组件(FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
@@ -532,92 +316,25 @@ export const submitForm = (options: SubmitFormOptions): Promise<any> => {
532
316
  }
533
317
  };
534
318
 
535
- // 调试模式:可见地渲染表单,点击「确定」才提交,点击「取消」则中断
536
- if (debug) {
537
- return createDebugWrapper({
538
- formRef,
539
- formProps,
540
- name: 'MFormSubmitWrapper',
541
- title: 'submitForm 调试',
542
- onConfirm: (setError) =>
543
- doSubmit((err) => {
544
- // 校验失败时保留弹层并展示错误,便于修正后重新提交
545
- setError(err instanceof Error ? err.message : String(err));
546
- }),
547
- onCancel: () => {
548
- reject(new Error('submitForm canceled in debug mode.'));
549
- cleanup();
550
- },
551
- });
552
- }
553
-
554
- // 普通模式:表单初始化完成后自动提交
555
- return defineComponent({
319
+ return createDialogWrapper({
320
+ formRef,
321
+ formProps,
556
322
  name: 'MFormSubmitWrapper',
557
- setup() {
558
- const stop = watch(
559
- () => formRef.value?.initialized,
560
- (initialized) => {
561
- if (!initialized) return;
562
- stop();
563
- doSubmit((err) => {
564
- reject(err);
565
- cleanup();
566
- });
567
- },
568
- { flush: 'post', immediate: true },
569
- );
570
-
571
- return () => h(Form as Component, { ...formProps, ref: formRef });
323
+ title: title ?? 'submitForm',
324
+ onConfirm: (setError) =>
325
+ doSubmit((err) => {
326
+ // 校验失败时保留弹层并展示错误,便于修正后重新提交
327
+ setError(err instanceof Error ? err.message : String(err));
328
+ }),
329
+ onCancel: () => {
330
+ reject(new Error('submitForm canceled.'));
331
+ cleanup();
572
332
  },
573
333
  });
574
334
  },
575
335
  });
576
336
  };
577
337
 
578
- // #region ValidateFormOptions
579
- /**
580
- * validateForm 函数参数(与 Form.vue 组件 props 对齐,取校验所需子集)
581
- */
582
- export interface ValidateFormOptions {
583
- /** 表单配置 */
584
- config: FormConfig;
585
- /** 待校验的表单值 */
586
- initValues?: Record<string, any>;
587
- parentValues?: Record<string, any>;
588
- labelWidth?: string;
589
- keyProp?: string;
590
- /**
591
- * 校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
592
- * 默认 `true`,置为 `false` 时直接使用字段 name。
593
- */
594
- useFieldTextInError?: boolean;
595
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
596
- /**
597
- * 父级应用上下文,用于继承全局组件、指令、provide 等。
598
- * 通常通过 `app._context` 或 `getCurrentInstance()?.appContext` 获取。
599
- */
600
- appContext?: AppContext | null;
601
- /** 等待表单初始化的最长时间(毫秒),超时将以错误 reject。默认 10000ms */
602
- timeout?: number;
603
- /**
604
- * 调试模式。默认 `false`。
605
- *
606
- * - `false`:以隐藏方式挂载,初始化完成后自动校验并 resolve 错误文案(原有静默行为)。
607
- * - `true`:将表单以弹层形式可见地渲染在页面上,需手动点击「确定」才会触发校验,
608
- * 点击「取消」则以 reject 中断;校验失败时在弹层内展示错误信息并保留弹层,便于修正后重试,
609
- * 校验通过则 resolve 空字符串。调试模式下 `timeout` 不生效(等待人工操作)。
610
- */
611
- debug?: boolean;
612
- typeMatchValid?: boolean;
613
- /**
614
- * 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
615
- * 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
616
- */
617
- signal?: AbortSignal;
618
- }
619
- // #endregion ValidateFormOptions
620
-
621
338
  // #region stripTabLazy
622
339
  /**
623
340
  * 深拷贝配置值,保留函数(display / onTabClick 等回调)引用,避免破坏配置中的回调。
@@ -673,44 +390,12 @@ export const stripTabItemsLazy = (config: FormConfig): FormConfig => {
673
390
  // #endregion stripTabLazy
674
391
 
675
392
  /**
676
- * 以命令式方式对一份「表单配置 + 值」做一次静默校验,**不依赖也不影响任何已渲染的表单**。
677
- *
678
- * 与 `submitForm` 类似,内部会临时挂载一个不可见的 MForm 实例,等待其初始化完成后调用
679
- * 实例的 `validate` 方法,返回汇总后的错误文案,随后自动卸载实例。
680
- *
681
- * 与 `submitForm` 的区别:
682
- * - 「静默」:校验失败不抛异常、不触发 `error` 事件、不返回表单值;
683
- * - 仅用于「探测」配置是否合法,适合源码保存后校验、批量校验组件配置等场景。
684
- *
685
- * 由于每次都新建一个独立的 MForm 实例,调用方无需持有任何表单 ref,也不会污染
686
- * 页面上正在展示的表单状态。
393
+ * 可见弹层校验:把表单渲染在弹层里,点击「确定」才调用挂载实例的 `validate`。
687
394
  *
688
- * @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
689
- * 仅在初始化超时或挂载失败等异常情况下才会 reject。
690
- *
691
- * @example
692
- * ```ts
693
- * import { validateForm } from '@tmagic/form';
694
- *
695
- * const error = await validateForm({
696
- * config: [...],
697
- * initValues: { name: 'foo' },
698
- * appContext: getCurrentInstance()?.appContext,
699
- * });
700
- * if (error) {
701
- * // 配置不合法,error 为错误文案
702
- * }
703
- *
704
- * // 调试模式:可见地渲染表单,点击「确定」才校验,校验失败保留弹层可修正重试:
705
- * const error = await validateForm({
706
- * config: [...],
707
- * initValues: { name: 'foo' },
708
- * debug: true,
709
- * });
710
- * ```
395
+ * 用于需要用户填写/确认的场景;默认路径仍是无渲染校验(见 `validateForm` 的说明)。
711
396
  */
712
- export const validateForm = (options: ValidateFormOptions): Promise<string> => {
713
- const { appContext, timeout = 10000, debug = false, config, signal, ...rest } = options;
397
+ const validateFormByDialogRender = (options: ValidateFormOptions): Promise<string> => {
398
+ const { appContext, config, signal, dialog, title, ...rest } = options;
714
399
 
715
400
  // 去掉 tab 容器各标签页的 lazy,确保懒加载标签页内的字段也参与校验
716
401
  const formProps = { ...rest, config: stripTabItemsLazy(config) };
@@ -718,19 +403,9 @@ export const validateForm = (options: ValidateFormOptions): Promise<string> => {
718
403
  return mountFormInstance<string>({
719
404
  formProps,
720
405
  appContext,
721
- timeout,
722
406
  signal,
723
- // 调试模式需把表单展示出来;普通模式隐藏挂载
724
- hidden: !debug,
725
- // 调试模式等待人工操作,不应用超时
726
- skipTimeout: debug,
727
- timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
728
407
  createWrapper: ({ formRef, formProps, cleanup, resolve, reject }) => {
729
- /**
730
- * 执行一次校验:nextTick 等待子组件渲染 → 调用实例 validate → 通过则 resolve ''。
731
- * 校验失败(返回非空错误文案)时交给 `onInvalid` 决定「静默 resolve 错误文案(普通)」
732
- * 还是「在弹层展示错误并保留供重试(debug)」,从而让两种模式共享同一份校验逻辑。
733
- */
408
+ // 执行一次校验:nextTick 等待子组件渲染 → 调用实例 validate → 通过则 resolve '',失败则在弹层展示错误
734
409
  const doValidate = async (onInvalid: (error: string) => void) => {
735
410
  try {
736
411
  // 等待子组件(FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
@@ -749,46 +424,43 @@ export const validateForm = (options: ValidateFormOptions): Promise<string> => {
749
424
  }
750
425
  };
751
426
 
752
- // 调试模式:可见地渲染表单,点击「确定」才校验,点击「取消」则中断
753
- if (debug) {
754
- return createDebugWrapper({
755
- formRef,
756
- formProps,
757
- name: 'MFormValidateWrapper',
758
- title: 'validateForm 调试',
759
- onConfirm: (setError) =>
760
- doValidate((error) => {
761
- // 校验失败时保留弹层并展示错误,便于修正后重新校验
762
- setError(error);
763
- }),
764
- onCancel: () => {
765
- reject(new Error('validateForm canceled in debug mode.'));
766
- cleanup();
767
- },
768
- });
769
- }
770
-
771
- // 普通模式:表单初始化完成后自动校验(静默 resolve 错误文案)
772
- return defineComponent({
427
+ return createDialogWrapper({
428
+ formRef,
429
+ formProps,
773
430
  name: 'MFormValidateWrapper',
774
- setup() {
775
- const stop = watch(
776
- () => formRef.value?.initialized,
777
- (initialized) => {
778
- if (!initialized) return;
779
- stop();
780
- doValidate((error) => {
781
- // 静默:校验失败也以错误文案 resolve(不抛异常)
782
- resolve(error);
783
- cleanup();
784
- });
785
- },
786
- { flush: 'post', immediate: true },
787
- );
788
-
789
- return () => h(Form as Component, { ...formProps, ref: formRef });
431
+ title: title ?? 'validateForm',
432
+ onConfirm: (setError) =>
433
+ doValidate((error) => {
434
+ // 校验失败时保留弹层并展示错误,便于修正后重新校验
435
+ setError(error);
436
+ }),
437
+ onCancel: () => {
438
+ reject(new Error('validateForm canceled.'));
439
+ cleanup();
790
440
  },
791
441
  });
792
442
  },
793
443
  });
794
444
  };
445
+
446
+ // #region headless
447
+ /**
448
+ * 以命令式方式对一份「表单配置 + 值」做一次校验并取回表单值。
449
+ *
450
+ * 默认走无渲染实现(见 `@tmagic/form/headless`)。`dialog: true` 会把表单以弹层渲染出来。
451
+ */
452
+ export const submitForm = async (options: SubmitFormOptions): Promise<any> => {
453
+ if (options.dialog) return submitFormByDialogRender(options);
454
+ return submitFormHeadless(options);
455
+ };
456
+
457
+ /**
458
+ * 以命令式方式对一份「表单配置 + 值」做一次静默校验。
459
+ *
460
+ * 默认走无渲染实现。`dialog: true` 的处理详见 `submitForm`。
461
+ */
462
+ export const validateForm = async (options: ValidateFormOptions): Promise<string> => {
463
+ if (options.dialog) return validateFormByDialogRender(options);
464
+ return validateFormHeadless(options);
465
+ };
466
+ // #endregion headless