@tmagic/form 1.8.0-beta.2 → 1.8.0-beta.21

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 (91) hide show
  1. package/dist/es/Form.vue_vue_type_script_setup_true_lang.js +120 -27
  2. package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +19 -3
  3. package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +30 -7
  4. package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +56 -14
  5. package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +3 -2
  6. package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +51 -36
  7. package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +5 -3
  8. package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +1 -1
  9. package/dist/es/containers/FormLabel.vue_vue_type_script_setup_true_lang.js +1 -1
  10. package/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +7 -6
  11. package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +42 -32
  12. package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +17 -11
  13. package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +1 -1
  14. package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +1 -1
  15. package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +1 -1
  16. package/dist/es/containers/table/ActionsColumn.vue_vue_type_script_setup_true_lang.js +14 -7
  17. package/dist/es/containers/table/SortColumn.vue_vue_type_script_setup_true_lang.js +1 -1
  18. package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +7 -5
  19. package/dist/es/containers/table/useTableColumns.js +5 -1
  20. package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +3 -2
  21. package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +6 -3
  22. package/dist/es/fields/Checkbox.vue_vue_type_script_setup_true_lang.js +5 -2
  23. package/dist/es/fields/CheckboxGroup.vue_vue_type_script_setup_true_lang.js +6 -3
  24. package/dist/es/fields/ColorPicker.vue_vue_type_script_setup_true_lang.js +5 -2
  25. package/dist/es/fields/Date.vue_vue_type_script_setup_true_lang.js +6 -3
  26. package/dist/es/fields/DateTime.vue_vue_type_script_setup_true_lang.js +6 -3
  27. package/dist/es/fields/Daterange.vue_vue_type_script_setup_true_lang.js +6 -3
  28. package/dist/es/fields/Display.vue_vue_type_script_setup_true_lang.js +6 -3
  29. package/dist/es/fields/DynamicField.vue_vue_type_script_setup_true_lang.js +5 -2
  30. package/dist/es/fields/Hidden.vue_vue_type_script_setup_true_lang.js +1 -1
  31. package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +30 -8
  32. package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +8 -3
  33. package/dist/es/fields/NumberRange.vue_vue_type_script_setup_true_lang.js +5 -2
  34. package/dist/es/fields/RadioGroup.vue_vue_type_script_setup_true_lang.js +21 -10
  35. package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +7 -3
  36. package/dist/es/fields/Switch.vue_vue_type_script_setup_true_lang.js +5 -2
  37. package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +113 -78
  38. package/dist/es/fields/Textarea.vue_vue_type_script_setup_true_lang.js +5 -2
  39. package/dist/es/fields/Time.vue_vue_type_script_setup_true_lang.js +5 -2
  40. package/dist/es/fields/Timerange.vue_vue_type_script_setup_true_lang.js +7 -4
  41. package/dist/es/index.js +5 -4
  42. package/dist/es/plugin.js +4 -0
  43. package/dist/es/schema.js +12 -1
  44. package/dist/es/style.css +133 -11
  45. package/dist/es/submitForm.js +415 -56
  46. package/dist/es/utils/config.js +4 -1
  47. package/dist/es/utils/form.js +84 -9
  48. package/dist/es/utils/typeMatch.js +487 -0
  49. package/dist/style.css +133 -11
  50. package/dist/themes/magic-admin.css +509 -0
  51. package/dist/tmagic-form.umd.cjs +1773 -399
  52. package/package.json +5 -5
  53. package/src/Form.vue +163 -30
  54. package/src/FormBox.vue +11 -0
  55. package/src/FormDialog.vue +25 -3
  56. package/src/FormDrawer.vue +51 -12
  57. package/src/containers/Col.vue +1 -0
  58. package/src/containers/Container.vue +32 -11
  59. package/src/containers/Fieldset.vue +5 -2
  60. package/src/containers/GroupList.vue +1 -1
  61. package/src/containers/GroupListItem.vue +28 -22
  62. package/src/containers/Panel.vue +10 -3
  63. package/src/containers/table/ActionsColumn.vue +10 -4
  64. package/src/containers/table/Table.vue +1 -0
  65. package/src/containers/table/useTableColumns.ts +7 -3
  66. package/src/containers/table-group-list/TableGroupList.vue +7 -1
  67. package/src/fields/Link.vue +16 -3
  68. package/src/fields/Number.vue +2 -1
  69. package/src/fields/RadioGroup.vue +24 -6
  70. package/src/fields/Text.vue +43 -19
  71. package/src/index.ts +11 -0
  72. package/src/plugin.ts +12 -0
  73. package/src/schema.ts +13 -1
  74. package/src/submitForm.ts +699 -66
  75. package/src/theme/container.scss +3 -0
  76. package/src/theme/fieldset.scss +51 -0
  77. package/src/theme/form-dialog.scss +1 -1
  78. package/src/theme/form.scss +6 -0
  79. package/src/theme/group-list.scss +19 -8
  80. package/src/theme/index.scss +1 -0
  81. package/src/theme/link.scss +17 -0
  82. package/src/theme/panel.scss +1 -1
  83. package/src/theme/radio-group.scss +23 -0
  84. package/src/theme/table.scss +11 -1
  85. package/src/theme/tabs.scss +2 -2
  86. package/src/theme/text.scss +38 -0
  87. package/src/theme/themes/magic-admin/index.scss +106 -0
  88. package/src/utils/config.ts +7 -1
  89. package/src/utils/form.ts +144 -22
  90. package/src/utils/typeMatch.ts +870 -0
  91. package/types/index.d.ts +408 -21
@@ -1,6 +1,200 @@
1
+ import { FORM_SILENT_MODE_KEY } from "./schema.js";
2
+ import { applyExtendState } from "./utils/form.js";
1
3
  import Form_default from "./Form.js";
2
- import { createApp, defineComponent, h, nextTick, ref, watch } from "vue";
4
+ import { createApp, defineComponent, h, nextTick, provide, ref, watch } from "vue";
3
5
  //#region packages/form/src/submitForm.ts
6
+ /** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
7
+ var DEFAULT_MOUNT_TIMEOUT = 1e4;
8
+ /**
9
+ * submitForm / validateForm 的公共脚手架:
10
+ *
11
+ * 创建临时容器 → 挂载一个 wrapper 组件(内含 MForm)→ 提供统一的 cleanup / timeout / appContext 继承 →
12
+ * 由 `createWrapper` 决定「何时调用 MForm 的哪个方法、如何 resolve/reject」。
13
+ *
14
+ * 调用方只需关注差异逻辑(调用 `submitForm` 还是 `validate`、结果如何包装、是否需要调试弹层),
15
+ * 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
16
+ */
17
+ var mountFormInstance = (options) => {
18
+ const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
19
+ return new Promise((resolve, reject) => {
20
+ if (signal?.aborted) {
21
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
22
+ return;
23
+ }
24
+ let cleaned = false;
25
+ let timer = null;
26
+ let onAbort = null;
27
+ const instance = { app: null };
28
+ const container = document.createElement("div");
29
+ if (hidden) container.style.display = "none";
30
+ document.body.appendChild(container);
31
+ const cleanup = () => {
32
+ if (cleaned) return;
33
+ cleaned = true;
34
+ if (timer) {
35
+ clearTimeout(timer);
36
+ timer = null;
37
+ }
38
+ if (signal && onAbort) {
39
+ signal.removeEventListener("abort", onAbort);
40
+ onAbort = null;
41
+ }
42
+ try {
43
+ instance.app?.unmount();
44
+ } catch {}
45
+ container.parentNode?.removeChild(container);
46
+ };
47
+ if (signal) {
48
+ onAbort = () => {
49
+ if (cleaned) return;
50
+ reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
51
+ cleanup();
52
+ };
53
+ signal.addEventListener("abort", onAbort);
54
+ }
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);
103
+ app.mount(container);
104
+ } catch (err) {
105
+ reject(err);
106
+ cleanup();
107
+ }
108
+ });
109
+ };
110
+ /**
111
+ * 构造调试模式下的可见弹层 wrapper:以 fixed 遮罩居中渲染 MForm,提供「确定 / 取消」按钮与错误展示区。
112
+ *
113
+ * submitForm 与 validateForm 的调试弹层 UI 完全一致,仅「确定」时调用的实例方法、错误处理与取消文案不同,
114
+ * 这些差异通过 `onConfirm` / `onCancel` 注入,弹层结构在此统一收口。
115
+ */
116
+ var createDebugWrapper = (options) => {
117
+ const { formRef, formProps, title, name, onConfirm, onCancel } = options;
118
+ const btnBase = {
119
+ padding: "8px 20px",
120
+ fontSize: "14px",
121
+ lineHeight: "1",
122
+ border: "1px solid #dcdfe6",
123
+ borderRadius: "4px",
124
+ cursor: "pointer"
125
+ };
126
+ return defineComponent({
127
+ name,
128
+ setup() {
129
+ const errorMsg = ref("");
130
+ const setError = (msg) => {
131
+ errorMsg.value = msg;
132
+ };
133
+ return () => h("div", { style: {
134
+ position: "fixed",
135
+ inset: "0",
136
+ zIndex: "10000",
137
+ display: "flex",
138
+ alignItems: "center",
139
+ justifyContent: "center",
140
+ background: "rgba(0, 0, 0, 0.5)"
141
+ } }, [h("div", { style: {
142
+ display: "flex",
143
+ flexDirection: "column",
144
+ width: "600px",
145
+ maxWidth: "90vw",
146
+ maxHeight: "85vh",
147
+ background: "#fff",
148
+ borderRadius: "8px",
149
+ boxShadow: "0 8px 24px rgba(0, 0, 0, 0.2)",
150
+ overflow: "hidden"
151
+ } }, [
152
+ h("div", { style: {
153
+ padding: "16px 20px",
154
+ fontSize: "16px",
155
+ fontWeight: "600",
156
+ borderBottom: "1px solid #ebeef5"
157
+ } }, title),
158
+ h("div", { style: {
159
+ flex: "1",
160
+ padding: "20px",
161
+ overflow: "auto"
162
+ } }, [h(Form_default, {
163
+ ...formProps,
164
+ ref: formRef
165
+ }), errorMsg.value ? h("div", {
166
+ style: {
167
+ marginTop: "12px",
168
+ color: "#f56c6c",
169
+ fontSize: "13px",
170
+ lineHeight: "1.5"
171
+ },
172
+ innerHTML: errorMsg.value
173
+ }) : null]),
174
+ h("div", { style: {
175
+ display: "flex",
176
+ justifyContent: "flex-end",
177
+ gap: "12px",
178
+ padding: "12px 20px",
179
+ borderTop: "1px solid #ebeef5"
180
+ } }, [h("button", {
181
+ type: "button",
182
+ onClick: onCancel,
183
+ style: { ...btnBase }
184
+ }, "取消"), h("button", {
185
+ type: "button",
186
+ onClick: () => onConfirm(setError),
187
+ style: {
188
+ ...btnBase,
189
+ color: "#fff",
190
+ background: "#409eff",
191
+ borderColor: "#409eff"
192
+ }
193
+ }, "确定")])
194
+ ])]);
195
+ }
196
+ });
197
+ };
4
198
  /**
5
199
  * 以命令式方式调用 Form.vue 完成一次表单校验/提交。
6
200
  *
@@ -21,67 +215,232 @@ import { createApp, defineComponent, h, nextTick, ref, watch } from "vue";
21
215
  * } catch (e) {
22
216
  * console.error(e);
23
217
  * }
218
+ *
219
+ * // 需要同时获取变更记录时:
220
+ * const { values, changeRecords } = await submitForm({
221
+ * config: [...],
222
+ * initValues: { name: 'foo' },
223
+ * returnChangeRecords: true,
224
+ * });
225
+ *
226
+ * // 调试模式:可见地渲染表单,点击「确定」才提交:
227
+ * const values = await submitForm({
228
+ * config: [...],
229
+ * initValues: { name: 'foo' },
230
+ * debug: true,
231
+ * });
24
232
  * ```
25
233
  */
26
234
  var submitForm = (options) => {
27
- const { native, appContext, timeout = 1e4, ...formProps } = options;
28
- return new Promise((resolve, reject) => {
29
- const container = document.createElement("div");
30
- container.style.display = "none";
31
- document.body.appendChild(container);
32
- let cleaned = false;
33
- let timer = null;
34
- const app = createApp(defineComponent({
35
- name: "MFormSubmitWrapper",
36
- setup() {
37
- const formRef = ref(null);
38
- const stop = watch(() => formRef.value?.initialized, async (initialized) => {
39
- if (!initialized) return;
40
- stop();
41
- try {
42
- await nextTick();
43
- resolve(await formRef.value.submitForm(native));
44
- } catch (err) {
45
- reject(err);
46
- } finally {
235
+ const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
236
+ return mountFormInstance({
237
+ formProps,
238
+ appContext,
239
+ timeout,
240
+ signal,
241
+ hidden: !debug,
242
+ skipTimeout: debug,
243
+ timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
244
+ createWrapper: ({ formRef, formProps, cleanup, resolve, reject }) => {
245
+ /**
246
+ * 执行一次提交:nextTick 等待子组件渲染 快照 changeRecords 调用实例 submitForm → resolve。
247
+ * 校验失败时交给 `onValidateError` 决定「保留弹层展示错误(debug)」还是「reject 并清理(普通)」,
248
+ * 从而让 debug 与普通模式共享同一份提交逻辑。
249
+ */
250
+ const doSubmit = async (onValidateError) => {
251
+ try {
252
+ await nextTick();
253
+ const changeRecords = [...formRef.value?.changeRecords ?? []];
254
+ const result = await formRef.value.submitForm(native);
255
+ resolve(returnChangeRecords ? {
256
+ values: result,
257
+ changeRecords
258
+ } : result);
259
+ cleanup();
260
+ } catch (err) {
261
+ onValidateError(err);
262
+ }
263
+ };
264
+ if (debug) return createDebugWrapper({
265
+ formRef,
266
+ formProps,
267
+ name: "MFormSubmitWrapper",
268
+ title: "submitForm 调试",
269
+ onConfirm: (setError) => doSubmit((err) => {
270
+ setError(err instanceof Error ? err.message : String(err));
271
+ }),
272
+ onCancel: () => {
273
+ reject(/* @__PURE__ */ new Error("submitForm canceled in debug mode."));
274
+ cleanup();
275
+ }
276
+ });
277
+ return defineComponent({
278
+ name: "MFormSubmitWrapper",
279
+ setup() {
280
+ const stop = watch(() => formRef.value?.initialized, (initialized) => {
281
+ if (!initialized) return;
282
+ stop();
283
+ doSubmit((err) => {
284
+ reject(err);
285
+ cleanup();
286
+ });
287
+ }, {
288
+ flush: "post",
289
+ immediate: true
290
+ });
291
+ return () => h(Form_default, {
292
+ ...formProps,
293
+ ref: formRef
294
+ });
295
+ }
296
+ });
297
+ }
298
+ });
299
+ };
300
+ /**
301
+ * 深拷贝配置值,保留函数(display / onTabClick 等回调)引用,避免破坏配置中的回调。
302
+ */
303
+ var cloneConfigValue = (value) => {
304
+ if (Array.isArray(value)) return value.map(cloneConfigValue);
305
+ if (value && typeof value === "object") return Object.keys(value).reduce((acc, key) => {
306
+ acc[key] = cloneConfigValue(value[key]);
307
+ return acc;
308
+ }, {});
309
+ return value;
310
+ };
311
+ /**
312
+ * 深度遍历配置,去掉所有 type 为 'tab' 的容器中各标签页(items)的 lazy 配置。
313
+ */
314
+ var removeTabItemsLazy = (node) => {
315
+ if (Array.isArray(node)) {
316
+ node.forEach(removeTabItemsLazy);
317
+ return;
318
+ }
319
+ if (!node || typeof node !== "object") return;
320
+ if (node.type === "tab" && Array.isArray(node.items)) node.items.forEach((pane) => {
321
+ if (pane && typeof pane === "object") delete pane.lazy;
322
+ });
323
+ Object.keys(node).forEach((key) => {
324
+ removeTabItemsLazy(node[key]);
325
+ });
326
+ };
327
+ /**
328
+ * 返回一份去除了 tab 标签页 lazy 的配置副本。
329
+ *
330
+ * tab 容器开启 lazy 时,非激活标签页的内容不会渲染,导致 validateForm 静默挂载后
331
+ * 无法校验到这些标签页内的字段。校验场景需要一次性渲染全部字段,故在此统一去除 lazy。
332
+ * 处理基于深拷贝,不会污染调用方传入的原始 config。
333
+ */
334
+ var stripTabItemsLazy = (config) => {
335
+ const cloned = cloneConfigValue(config);
336
+ removeTabItemsLazy(cloned);
337
+ return cloned;
338
+ };
339
+ /**
340
+ * 以命令式方式对一份「表单配置 + 值」做一次静默校验,**不依赖也不影响任何已渲染的表单**。
341
+ *
342
+ * 与 `submitForm` 类似,内部会临时挂载一个不可见的 MForm 实例,等待其初始化完成后调用
343
+ * 实例的 `validate` 方法,返回汇总后的错误文案,随后自动卸载实例。
344
+ *
345
+ * 与 `submitForm` 的区别:
346
+ * - 「静默」:校验失败不抛异常、不触发 `error` 事件、不返回表单值;
347
+ * - 仅用于「探测」配置是否合法,适合源码保存后校验、批量校验组件配置等场景。
348
+ *
349
+ * 由于每次都新建一个独立的 MForm 实例,调用方无需持有任何表单 ref,也不会污染
350
+ * 页面上正在展示的表单状态。
351
+ *
352
+ * @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
353
+ * 仅在初始化超时或挂载失败等异常情况下才会 reject。
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * import { validateForm } from '@tmagic/form';
358
+ *
359
+ * const error = await validateForm({
360
+ * config: [...],
361
+ * initValues: { name: 'foo' },
362
+ * appContext: getCurrentInstance()?.appContext,
363
+ * });
364
+ * if (error) {
365
+ * // 配置不合法,error 为错误文案
366
+ * }
367
+ *
368
+ * // 调试模式:可见地渲染表单,点击「确定」才校验,校验失败保留弹层可修正重试:
369
+ * const error = await validateForm({
370
+ * config: [...],
371
+ * initValues: { name: 'foo' },
372
+ * debug: true,
373
+ * });
374
+ * ```
375
+ */
376
+ var validateForm = (options) => {
377
+ const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
378
+ return mountFormInstance({
379
+ formProps: {
380
+ ...rest,
381
+ config: stripTabItemsLazy(config)
382
+ },
383
+ appContext,
384
+ timeout,
385
+ signal,
386
+ hidden: !debug,
387
+ skipTimeout: debug,
388
+ timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
389
+ createWrapper: ({ formRef, formProps, cleanup, resolve, reject }) => {
390
+ /**
391
+ * 执行一次校验:nextTick 等待子组件渲染 → 调用实例 validate → 通过则 resolve ''。
392
+ * 校验失败(返回非空错误文案)时交给 `onInvalid` 决定「静默 resolve 错误文案(普通)」
393
+ * 还是「在弹层展示错误并保留供重试(debug)」,从而让两种模式共享同一份校验逻辑。
394
+ */
395
+ const doValidate = async (onInvalid) => {
396
+ try {
397
+ await nextTick();
398
+ const error = await formRef.value.validate();
399
+ if (error) onInvalid(error);
400
+ else {
401
+ resolve("");
47
402
  cleanup();
48
403
  }
49
- }, {
50
- flush: "post",
51
- immediate: true
52
- });
53
- return () => h(Form_default, {
54
- ...formProps,
55
- ref: formRef
56
- });
57
- }
58
- }));
59
- if (appContext) Object.assign(app._context, appContext);
60
- const cleanup = () => {
61
- if (cleaned) return;
62
- cleaned = true;
63
- if (timer) {
64
- clearTimeout(timer);
65
- timer = null;
66
- }
67
- try {
68
- app.unmount();
69
- } catch {}
70
- container.parentNode?.removeChild(container);
71
- };
72
- if (timeout > 0) timer = setTimeout(() => {
73
- if (!cleaned) {
74
- reject(/* @__PURE__ */ new Error(`submitForm timeout after ${timeout}ms: form is not initialized.`));
75
- cleanup();
76
- }
77
- }, timeout);
78
- try {
79
- app.mount(container);
80
- } catch (err) {
81
- reject(err);
82
- cleanup();
404
+ } catch (err) {
405
+ reject(err);
406
+ cleanup();
407
+ }
408
+ };
409
+ if (debug) return createDebugWrapper({
410
+ formRef,
411
+ formProps,
412
+ name: "MFormValidateWrapper",
413
+ title: "validateForm 调试",
414
+ onConfirm: (setError) => doValidate((error) => {
415
+ setError(error);
416
+ }),
417
+ onCancel: () => {
418
+ reject(/* @__PURE__ */ new Error("validateForm canceled in debug mode."));
419
+ cleanup();
420
+ }
421
+ });
422
+ return defineComponent({
423
+ name: "MFormValidateWrapper",
424
+ setup() {
425
+ const stop = watch(() => formRef.value?.initialized, (initialized) => {
426
+ if (!initialized) return;
427
+ stop();
428
+ doValidate((error) => {
429
+ resolve(error);
430
+ cleanup();
431
+ });
432
+ }, {
433
+ flush: "post",
434
+ immediate: true
435
+ });
436
+ return () => h(Form_default, {
437
+ ...formProps,
438
+ ref: formRef
439
+ });
440
+ }
441
+ });
83
442
  }
84
443
  });
85
444
  };
86
445
  //#endregion
87
- export { submitForm };
446
+ export { stripTabItemsLazy, submitForm, validateForm };
@@ -1,7 +1,10 @@
1
+ import { ref } from "vue";
1
2
  //#region packages/form/src/utils/config.ts
2
3
  var $MAGIC_FORM = {};
4
+ var isGlobalFlat = ref(false);
3
5
  var setConfig = (option) => {
4
6
  $MAGIC_FORM = option;
7
+ isGlobalFlat.value = option.flat ?? false;
5
8
  };
6
9
  var getConfig = (key) => $MAGIC_FORM[key];
7
10
  var fieldRegistry = /* @__PURE__ */ new Map();
@@ -12,4 +15,4 @@ var registerField = (tagName, component) => {
12
15
  var getField = (tagName) => fieldRegistry.get(tagName);
13
16
  var deleteField = (tagName) => fieldRegistry.delete(tagName);
14
17
  //#endregion
15
- export { deleteField, getConfig, getField, registerField, setConfig };
18
+ export { deleteField, getConfig, getField, isGlobalFlat, registerField, setConfig };
@@ -1,10 +1,46 @@
1
+ import { createTypeMatchValidator } from "./typeMatch.js";
1
2
  import { readonly } from "vue";
2
- import { cloneDeep } from "lodash-es";
3
- import { getValueByKeyPath } from "@tmagic/utils";
4
3
  import dayjs from "dayjs";
5
4
  import utc from "dayjs/plugin/utc";
5
+ import { cloneDeep } from "lodash-es";
6
+ import { getDesignConfig } from "@tmagic/design";
7
+ import { getValueByKeyPath } from "@tmagic/utils";
6
8
  //#region packages/form/src/utils/form.ts
7
- var TABLE_SELECT_TYPES = new Set(["table-select", "tableSelect"]);
9
+ var isTDesignAdapter = () => getDesignConfig("adapterType") === "tdesign-vue-next";
10
+ /**
11
+ * 将 async-validator(Element Plus)风格的 validator 适配到当前 UI 库。
12
+ * TDesign 调用签名为 `(val) => boolean | CustomValidateObj | Promise`,无 callback。
13
+ */
14
+ var adaptFormValidator = (validator) => {
15
+ return (arg1, arg2, arg3, arg4, arg5) => {
16
+ if (!isTDesignAdapter()) return validator(arg1, arg2, arg3, arg4, arg5);
17
+ const value = arg1;
18
+ return new Promise((resolve) => {
19
+ let settled = false;
20
+ const callback = (error) => {
21
+ if (settled) return;
22
+ settled = true;
23
+ const first = Array.isArray(error) ? error[0] : error;
24
+ if (first) resolve({
25
+ result: false,
26
+ message: typeof first === "string" ? first : first.message
27
+ });
28
+ else resolve(true);
29
+ };
30
+ try {
31
+ const result = validator(void 0, value, callback);
32
+ if (result && typeof result.then === "function") Promise.resolve(result).then(() => {
33
+ if (!settled) callback();
34
+ }, (err) => {
35
+ callback(err instanceof Error ? err : new Error(String(err)));
36
+ });
37
+ } catch (e) {
38
+ callback(e instanceof Error ? e : new Error(String(e)));
39
+ }
40
+ });
41
+ };
42
+ };
43
+ var TABLE_SELECT_TYPES = /* @__PURE__ */ new Set(["table-select", "tableSelect"]);
8
44
  var isTableSelect = (type) => typeof type === "string" && TABLE_SELECT_TYPES.has(type);
9
45
  var asyncLoadConfig = (value, initValue, { asyncLoad, name, type }) => {
10
46
  if (type === "html" && typeof asyncLoad === "object" && typeof name !== "undefined") {
@@ -12,7 +48,7 @@ var asyncLoadConfig = (value, initValue, { asyncLoad, name, type }) => {
12
48
  value.asyncLoad = typeof initValue.asyncLoad === "object" ? initValue.asyncLoad : asyncLoad;
13
49
  }
14
50
  };
15
- var MULTIPLE_VALUE_TYPES = new Set([
51
+ var MULTIPLE_VALUE_TYPES = /* @__PURE__ */ new Set([
16
52
  "checkbox-group",
17
53
  "checkboxGroup",
18
54
  "table",
@@ -99,13 +135,18 @@ var display = function(mForm, config, props) {
99
135
  if (config === false) return false;
100
136
  return true;
101
137
  };
102
- var getRules = function(mForm, rules = [], props) {
103
- rules = cloneDeep(rules);
138
+ var getRules = function(mForm, r = [], props, typeMatchValid) {
139
+ let rules = cloneDeep(r);
104
140
  if (typeof rules === "object" && !Array.isArray(rules)) rules = [rules];
141
+ if (typeMatchValid?.value && !rules.some((r) => typeof r.typeMatch !== "undefined")) rules.push({ typeMatch: true });
105
142
  return rules.map((item) => {
143
+ if (item.typeMatch) {
144
+ item.validator = adaptFormValidator(createTypeMatchValidator(mForm, props, item));
145
+ return item;
146
+ }
106
147
  if (typeof item.validator === "function") {
107
148
  const fnc = item.validator;
108
- item.validator = (rule, value, callback, source, options) => fnc({
149
+ item.validator = adaptFormValidator((rule, value, callback, source, options) => fnc({
109
150
  rule,
110
151
  value: props.config.names ? props.model : value,
111
152
  callback,
@@ -118,7 +159,7 @@ var getRules = function(mForm, rules = [], props) {
118
159
  formValue: mForm?.values || props.model,
119
160
  prop: props.prop,
120
161
  config: props.config
121
- }, mForm);
162
+ }, mForm));
122
163
  }
123
164
  return item;
124
165
  });
@@ -163,6 +204,40 @@ var sortChange = (data, { prop, order }) => {
163
204
  if (order === "ascending") data.sort((a, b) => a[prop] - b[prop]);
164
205
  else if (order === "descending") data.sort((a, b) => b[prop] - a[prop]);
165
206
  };
207
+ /**
208
+ * 将 extendState 返回的扩展字段合并进 formState。
209
+ *
210
+ * - data 描述符(普通字段)通过 `formState[key] = value` 写入,走 reactive proxy 的 set,
211
+ * 触发依赖通知;
212
+ * - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
213
+ * 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
214
+ *
215
+ * 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
216
+ * 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
217
+ * config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
218
+ * post 等),命中这些 key 时统一跳过并告警。
219
+ *
220
+ * 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
221
+ * 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
222
+ * `TypeError: 'set' on proxy: trap returned falsish`。
223
+ */
224
+ var applyExtendState = (formState, state, reservedKeys) => {
225
+ if (!state) return;
226
+ for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
227
+ if (reservedKeys?.has(key)) continue;
228
+ if (!("value" in descriptor)) {
229
+ descriptor.configurable = true;
230
+ Object.defineProperty(formState, key, descriptor);
231
+ continue;
232
+ }
233
+ const targetDescriptor = Object.getOwnPropertyDescriptor(formState, key);
234
+ if (targetDescriptor && !("value" in targetDescriptor) && typeof targetDescriptor.set !== "function") {
235
+ console.warn(`[MForm] extendState: "${key}" is a read-only field derived from props and cannot be assigned a plain value. Return it as a getter accessor if you really need to override it.`);
236
+ continue;
237
+ }
238
+ formState[key] = state[key];
239
+ }
240
+ };
166
241
  var createObjectProp = (prop, key, name) => {
167
242
  if (prop === "") return key;
168
243
  const itemPath = `${prop}`.split(".");
@@ -172,4 +247,4 @@ var createObjectProp = (prop, key, name) => {
172
247
  return `${[...itemPath, key].join(".")}`;
173
248
  };
174
249
  //#endregion
175
- export { createObjectProp, createValues, datetimeFormatter, display, filterFunction, getDataByPage, getRules, initValue, sortArray, sortChange };
250
+ export { adaptFormValidator, applyExtendState, createObjectProp, createValues, datetimeFormatter, display, filterFunction, getDataByPage, getRules, initValue, sortArray, sortChange };