@tmagic/form 1.8.0-beta.24 → 1.8.0-beta.26

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 +2 -2
  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 +85 -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 +75 -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 +85 -1
  63. package/dist/themes/magic-admin.css +87 -7
  64. package/dist/tmagic-form-headless.umd.cjs +4044 -0
  65. package/dist/tmagic-form.umd.cjs +1975 -874
  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/headless.ts +78 -0
  102. package/src/index.ts +32 -15
  103. package/src/plugin.ts +63 -42
  104. package/src/schema.ts +6 -8
  105. package/src/submitForm.ts +81 -409
  106. package/src/theme/form-box.scss +5 -0
  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 +104 -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
@@ -0,0 +1,308 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2025 Tencent. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import type { App, Component } from 'vue';
20
+
21
+ import { toLine } from '@tmagic/utils';
22
+
23
+ import {
24
+ clearContainerWalkers,
25
+ type ContainerWalker,
26
+ deleteContainerWalker,
27
+ registerContainerWalker,
28
+ } from './collectFields';
29
+ import {
30
+ clearFieldInnerConfigs,
31
+ deleteFieldInnerConfig,
32
+ type FieldInnerConfig,
33
+ registerFieldInnerConfig,
34
+ } from './fieldInnerConfig';
35
+ import {
36
+ clearLeafFieldTypes,
37
+ deleteLeafFieldType,
38
+ type FieldMountValueEffect,
39
+ registerLeafFieldType,
40
+ } from './fieldValueEffects';
41
+ import { clearTypeMatchRules, deleteTypeMatchRule, registerTypeMatchRule, type TypeMatchValidator } from './typeMatch';
42
+
43
+ // #region FieldOptions
44
+ /**
45
+ * 自定义字段 type 的登记项。
46
+ *
47
+ * 不传或只传空对象:当作叶子,不再下钻 `items`。
48
+ */
49
+ export interface FieldOptions {
50
+ /**
51
+ * 渲染用的 Vue 组件,写入字段注册表。
52
+ * 传入 `app` 时同时 `app.component('m-fields-*')`。
53
+ */
54
+ component?: Component;
55
+ /**
56
+ * 容器组件,写入字段注册表。
57
+ * 传入 `app` 时同时 `app.component('m-form-*')`。
58
+ */
59
+ container?: Component;
60
+ /** 字段挂载时改写 model 的副作用。可与 `innerConfig` 同时登记。 */
61
+ effect?: FieldMountValueEffect;
62
+ /**
63
+ * 按容器模板遍历(tab / table 等)。
64
+ * 与 `innerConfig` / `effect` 同时传入时 `walk` 优先。
65
+ */
66
+ walk?: ContainerWalker;
67
+ /**
68
+ * 把内部会挂到父表单的配置交出来。
69
+ * 可与 `effect` 同时传入:`effect` 负责本字段的值初始化,`innerConfig` 只做配置派生。
70
+ */
71
+ innerConfig?: FieldInnerConfig;
72
+ /** 该 type 的 typeMatch 校验;可与叶子、walk 或 innerConfig 同时登记。 */
73
+ typeMatch?: TypeMatchValidator;
74
+ }
75
+
76
+ /**
77
+ * 无渲染校验用的登记项,不含 Vue 组件。
78
+ *
79
+ * 给 Node / `validateForm` / `submitForm` 用。
80
+ */
81
+ export type HeadlessFieldOptions = Omit<FieldOptions, 'component' | 'container'>;
82
+ // #endregion FieldOptions
83
+
84
+ const extraComponents = new Map<string, Component>();
85
+ const builtInComponents = new Map<string, Component>();
86
+
87
+ const applyVueComponent = (
88
+ type: string,
89
+ component: Component,
90
+ app: App | undefined,
91
+ builtIn: boolean,
92
+ kind: 'fields' | 'form',
93
+ ): void => {
94
+ const key = toLine(type);
95
+ if (builtIn) {
96
+ builtInComponents.set(key, component);
97
+ } else {
98
+ extraComponents.set(key, component);
99
+ }
100
+ app?.component(`m-${kind}-${key}`, component);
101
+ };
102
+
103
+ /**
104
+ * 把已登记字段的 Vue 组件写入注册表;传入 `app` 时同时挂 `m-fields-*` / `m-form-*`。
105
+ *
106
+ * @param fields - 字段登记表
107
+ * @param [app] - Vue 应用;省略则只写入注册表
108
+ * @param [builtIn=false] - 是否写入内置表(`clearFields` 不会清掉)
109
+ */
110
+ export const bindFieldApp = (fields: Record<string, FieldOptions>, app?: App, builtIn = false): void => {
111
+ for (const [type, options] of Object.entries(fields)) {
112
+ if (options?.component) {
113
+ applyVueComponent(type, options.component, app, builtIn, 'fields');
114
+ }
115
+ if (options?.container) {
116
+ applyVueComponent(type, options.container, app, builtIn, 'form');
117
+ }
118
+ }
119
+ };
120
+
121
+ /**
122
+ * 把多份字段登记表按 type 浅合并。后一份只覆盖自己带了的 key,未出现的 key 保留前一份。
123
+ *
124
+ * 安装插件时用:Node 侧只登记 `headless`,浏览器再补 `component` / `container`。
125
+ *
126
+ * @param tables - 待合并的登记表,`undefined` 会被跳过
127
+ * @returns 合并后的登记表
128
+ */
129
+ export const mergeFieldOptions = (
130
+ ...tables: Array<Record<string, HeadlessFieldOptions | FieldOptions> | undefined>
131
+ ): Record<string, FieldOptions> => {
132
+ const result: Record<string, FieldOptions> = {};
133
+ for (const table of tables) {
134
+ if (!table) continue;
135
+ for (const [type, options] of Object.entries(table)) {
136
+ result[type] = { ...result[type], ...pickDefinedFieldOptions(options) };
137
+ }
138
+ }
139
+ return result;
140
+ };
141
+
142
+ const FIELD_OPTION_KEYS = ['component', 'container', 'effect', 'walk', 'innerConfig', 'typeMatch'] as const;
143
+
144
+ const pickDefinedFieldOptions = (options?: FieldOptions): FieldOptions => {
145
+ if (!options) return {};
146
+ const next: FieldOptions = {};
147
+ for (const key of FIELD_OPTION_KEYS) {
148
+ if (options[key] !== undefined) {
149
+ (next as any)[key] = options[key];
150
+ }
151
+ }
152
+ return next;
153
+ };
154
+
155
+ const extraFieldOptions = new Map<string, FieldOptions>();
156
+ const builtInFieldOptions = new Map<string, FieldOptions>();
157
+
158
+ const removeFormComponent = (type: string): void => {
159
+ extraComponents.delete(toLine(type));
160
+ };
161
+
162
+ const clearFormComponents = (): void => {
163
+ extraComponents.clear();
164
+ };
165
+
166
+ /**
167
+ * 按字段 type 取已登记的渲染组件(`codeSelect` 与 `code-select` 等价)。
168
+ *
169
+ * @param type - 字段 type
170
+ * @returns 已登记的 Vue 组件;未登记则为 `undefined`
171
+ */
172
+ export const getField = (type: string): Component | undefined => {
173
+ const key = toLine(type);
174
+ return extraComponents.get(key) ?? builtInComponents.get(key);
175
+ };
176
+
177
+ const registerFieldImpl = (type: string, options: FieldOptions | undefined, app: App | undefined, builtIn: boolean) => {
178
+ if (typeof type !== 'string' || !type) return;
179
+
180
+ const key = toLine(type);
181
+ const store = builtIn ? builtInFieldOptions : extraFieldOptions;
182
+ const incoming = pickDefinedFieldOptions(options);
183
+ const merged: FieldOptions = { ...store.get(key), ...incoming };
184
+ store.set(key, merged);
185
+
186
+ if (incoming.walk && (incoming.innerConfig || typeof incoming.effect === 'function')) {
187
+ console.warn(
188
+ `[MForm] registerField("${key}"): walk is set together with innerConfig/effect; ` +
189
+ 'headless validation will use walk and innerConfig/effect will be ignored.',
190
+ );
191
+ }
192
+
193
+ if (incoming.component && incoming.container) {
194
+ console.warn(
195
+ `[MForm] registerField("${key}"): component and container are both set; ` +
196
+ 'getField will use container, and both m-fields-* / m-form-* will be registered.',
197
+ );
198
+ }
199
+
200
+ if (merged.component) {
201
+ applyVueComponent(type, merged.component, app, builtIn, 'fields');
202
+ }
203
+
204
+ if (merged.container) {
205
+ applyVueComponent(type, merged.container, app, builtIn, 'form');
206
+ }
207
+
208
+ if (merged.typeMatch) {
209
+ registerTypeMatchRule(type, merged.typeMatch, builtIn);
210
+ }
211
+
212
+ if (merged.walk) {
213
+ registerContainerWalker(type, merged.walk, builtIn);
214
+ if (!builtIn) {
215
+ deleteLeafFieldType(type);
216
+ deleteFieldInnerConfig(type);
217
+ }
218
+ return;
219
+ }
220
+
221
+ if (!builtIn) {
222
+ deleteContainerWalker(type);
223
+ }
224
+
225
+ if (merged.innerConfig) {
226
+ registerFieldInnerConfig(type, merged.innerConfig, builtIn);
227
+ if (typeof merged.effect === 'function') {
228
+ registerLeafFieldType(type, merged.effect, builtIn);
229
+ } else if (!builtIn) {
230
+ deleteLeafFieldType(type);
231
+ }
232
+ return;
233
+ }
234
+
235
+ // 只登记了容器组件:不当叶子,dispatch 会按 items 下钻
236
+ if (merged.container && !merged.component && typeof merged.effect !== 'function') {
237
+ if (!builtIn) deleteLeafFieldType(type);
238
+ return;
239
+ }
240
+
241
+ if (!builtIn) {
242
+ deleteFieldInnerConfig(type);
243
+ }
244
+ registerLeafFieldType(type, merged.effect, builtIn);
245
+ };
246
+
247
+ /**
248
+ * 登记一个字段 type 在无渲染校验里的行为,以及可选的渲染组件。
249
+ *
250
+ * `type` 会按 Container 的规则归一化为中划线形式(`codeSelect` 与 `code-select` 等价)。
251
+ * 对同一 type 重复登记按字段浅合并:后一次只覆盖自己传入的 key,未传入的 key 保留。
252
+ * 传入 `app` 且带了 `component` / `container` 时,会同步 `app.component('m-fields-*'` / `'m-form-*')`。
253
+ *
254
+ * @param type - 字段 type
255
+ * @param [options] - 登记项;省略或空对象视为叶子
256
+ * @param [app] - Vue 应用;省略则不调用 `app.component`
257
+ */
258
+ export const registerField = (type: string, options?: FieldOptions, app?: App): void => {
259
+ registerFieldImpl(type, options, app, false);
260
+ };
261
+
262
+ /**
263
+ * 批量登记字段。
264
+ *
265
+ * @param fields - type 到登记项的映射
266
+ * @param [app] - Vue 应用;省略则不调用 `app.component`
267
+ */
268
+ export const registerFields = (fields: Record<string, FieldOptions>, app?: App): void => {
269
+ for (const [type, options] of Object.entries(fields)) {
270
+ registerFieldImpl(type, options, app, false);
271
+ }
272
+ };
273
+
274
+ /**
275
+ * 登记内置字段(`clearFields` / `unregisterField` 不会清掉)。
276
+ *
277
+ * @param fields - type 到登记项的映射
278
+ * @param [app] - Vue 应用;省略则不调用 `app.component`
279
+ */
280
+ export const registerBuiltInFields = (fields: Record<string, FieldOptions>, app?: App): void => {
281
+ for (const [type, options] of Object.entries(fields)) {
282
+ registerFieldImpl(type, options, app, true);
283
+ }
284
+ };
285
+
286
+ /**
287
+ * 删除业务侧对指定 type 的登记(不影响内置;主要用于单测)。
288
+ *
289
+ * @param type - 字段 type
290
+ */
291
+ export const unregisterField = (type: string): void => {
292
+ extraFieldOptions.delete(toLine(type));
293
+ deleteLeafFieldType(type);
294
+ deleteFieldInnerConfig(type);
295
+ deleteTypeMatchRule(type);
296
+ deleteContainerWalker(type);
297
+ removeFormComponent(type);
298
+ };
299
+
300
+ /** 清空业务侧登记(不影响内置;主要用于单测)。 */
301
+ export const clearFields = (): void => {
302
+ extraFieldOptions.clear();
303
+ clearLeafFieldTypes();
304
+ clearFieldInnerConfigs();
305
+ clearTypeMatchRules();
306
+ clearContainerWalkers();
307
+ clearFormComponents();
308
+ };
@@ -0,0 +1,206 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2025 Tencent. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import type { AppContext } from 'vue';
20
+ import { cloneDeep } from 'lodash-es';
21
+
22
+ import type { ChangeRecord, FormConfig, FormContext } from '../schema';
23
+
24
+ import { validateValues, type ValidateValuesResult } from './validateValues';
25
+
26
+ // #region SubmitFormOptions
27
+ /**
28
+ * submitForm 函数参数(与 Form.vue 组件 props 对齐)
29
+ */
30
+ export interface SubmitFormOptions {
31
+ /** 表单配置 */
32
+ config: FormConfig;
33
+ /** 表单初始值 */
34
+ initValues?: Record<string, any>;
35
+ /** 需对比的值(开启对比模式时传入) */
36
+ lastValues?: Record<string, any>;
37
+ /** 是否开启对比模式 */
38
+ isCompare?: boolean;
39
+ parentValues?: Record<string, any>;
40
+ labelWidth?: string;
41
+ disabled?: boolean;
42
+ height?: string;
43
+ stepActive?: string | number;
44
+ size?: 'small' | 'default' | 'large';
45
+ inline?: boolean;
46
+ labelPosition?: 'top' | 'left' | 'right';
47
+ keyProp?: string;
48
+ popperClass?: string;
49
+ preventSubmitDefault?: boolean;
50
+ /**
51
+ * 表单校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
52
+ * 默认 `true`,置为 `false` 时直接使用字段 name。
53
+ */
54
+ useFieldTextInError?: boolean;
55
+ /** 宿主业务上下文,与 MForm 的同名 prop 语义一致 */
56
+ context?: FormContext;
57
+ /** 透传给 Form.submitForm 的参数:是否直接返回原始响应式 values */
58
+ native?: boolean;
59
+ /**
60
+ * 是否在 resolve 结果中携带 changeRecords(变更记录)。
61
+ * 开启后 resolve 的结果为 `{ values, changeRecords }`,否则仅 resolve values。
62
+ */
63
+ returnChangeRecords?: boolean;
64
+ /**
65
+ * 父级应用上下文,用于继承全局组件、指令、provide 等。
66
+ * 仅 `dialog: true` 时生效。`@tmagic/form/headless` 不支持弹层。
67
+ */
68
+ appContext?: AppContext | null;
69
+ /**
70
+ * 是否以弹层展示表单。默认 `false`。
71
+ *
72
+ * `@tmagic/form/headless` 不支持 `dialog: true`,请从 `@tmagic/form` 引入。
73
+ */
74
+ dialog?: boolean;
75
+ /**
76
+ * 弹层标题,仅 `dialog: true` 时生效。
77
+ */
78
+ title?: string;
79
+ typeMatchValid?: boolean;
80
+ /**
81
+ * 外部中断信号。abort 时会立即以 `signal.reason` reject。
82
+ */
83
+ signal?: AbortSignal;
84
+ }
85
+ // #endregion SubmitFormOptions
86
+
87
+ // #region SubmitFormResult
88
+ /**
89
+ * 开启 `returnChangeRecords` 时 submitForm 的返回结果
90
+ */
91
+ export interface SubmitFormResult {
92
+ /** 校验通过后的表单值 */
93
+ values: any;
94
+ /** 表单变更记录 */
95
+ changeRecords: ChangeRecord[];
96
+ }
97
+ // #endregion SubmitFormResult
98
+
99
+ // #region ValidateFormOptions
100
+ /**
101
+ * validateForm 函数参数(与 Form.vue 组件 props 对齐,取校验所需子集)
102
+ */
103
+ export interface ValidateFormOptions {
104
+ /** 表单配置 */
105
+ config: FormConfig;
106
+ /** 待校验的表单值 */
107
+ initValues?: Record<string, any>;
108
+ parentValues?: Record<string, any>;
109
+ labelWidth?: string;
110
+ keyProp?: string;
111
+ /**
112
+ * 校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
113
+ * 默认 `true`,置为 `false` 时直接使用字段 name。
114
+ */
115
+ useFieldTextInError?: boolean;
116
+ /** 宿主业务上下文,与 MForm 的同名 prop 语义一致 */
117
+ context?: FormContext;
118
+ /**
119
+ * 父级应用上下文。仅 `dialog: true` 时生效。`@tmagic/form/headless` 不支持弹层。
120
+ */
121
+ appContext?: AppContext | null;
122
+ /**
123
+ * 是否以弹层展示表单。默认 `false`。
124
+ *
125
+ * `@tmagic/form/headless` 不支持 `dialog: true`,请从 `@tmagic/form` 引入。
126
+ */
127
+ dialog?: boolean;
128
+ /**
129
+ * 弹层标题,仅 `dialog: true` 时生效。
130
+ */
131
+ title?: string;
132
+ typeMatchValid?: boolean;
133
+ /**
134
+ * 外部中断信号。abort 时会立即以 `signal.reason` reject。
135
+ */
136
+ signal?: AbortSignal;
137
+ }
138
+ // #endregion ValidateFormOptions
139
+
140
+ const throwIfAborted = (signal: AbortSignal | undefined, fnName: string) => {
141
+ if (signal?.aborted) {
142
+ throw signal.reason ?? new Error(`${fnName} aborted`);
143
+ }
144
+ };
145
+
146
+ const throwIfDialog = (dialog: boolean | undefined, fnName: string) => {
147
+ if (dialog) {
148
+ throw new Error(
149
+ `[MForm] ${fnName}({ dialog: true }) is not available from @tmagic/form/headless. Import from @tmagic/form instead.`,
150
+ );
151
+ }
152
+ };
153
+
154
+ /**
155
+ * `submitForm` 与 `validateForm` 共用的无渲染校验流程:中断检查 → `validateValues`。
156
+ */
157
+ export const validateWithoutRender = async (
158
+ fnName: 'submitForm' | 'validateForm',
159
+ options: SubmitFormOptions | ValidateFormOptions,
160
+ ): Promise<ValidateValuesResult> => {
161
+ const { signal, config, initValues, parentValues, keyProp, typeMatchValid, useFieldTextInError, context } = options;
162
+
163
+ throwIfAborted(signal, fnName);
164
+
165
+ const result = await validateValues({
166
+ config,
167
+ initValues,
168
+ parentValues,
169
+ keyProp,
170
+ popperClass: (options as SubmitFormOptions).popperClass,
171
+ typeMatchValid,
172
+ useFieldTextInError,
173
+ context,
174
+ });
175
+
176
+ throwIfAborted(signal, fnName);
177
+
178
+ return result;
179
+ };
180
+
181
+ /**
182
+ * 以命令式方式对一份「表单配置 + 值」做一次校验并取回表单值(无渲染)。
183
+ *
184
+ * `@tmagic/form/headless` 不支持 `dialog: true`。
185
+ */
186
+ export const submitForm = async (options: SubmitFormOptions): Promise<any> => {
187
+ throwIfDialog(options.dialog, 'submitForm');
188
+
189
+ const validated = await validateWithoutRender('submitForm', options);
190
+
191
+ if (validated.error) throw new Error(validated.error);
192
+
193
+ const values = options.native ? validated.values : cloneDeep(validated.values);
194
+ return options.returnChangeRecords ? { values, changeRecords: [] as ChangeRecord[] } : values;
195
+ };
196
+
197
+ /**
198
+ * 以命令式方式对一份「表单配置 + 值」做一次静默校验(无渲染)。
199
+ *
200
+ * `@tmagic/form/headless` 不支持 `dialog: true`。
201
+ */
202
+ export const validateForm = async (options: ValidateFormOptions): Promise<string> => {
203
+ throwIfDialog(options.dialog, 'validateForm');
204
+
205
+ return (await validateWithoutRender('validateForm', options)).error;
206
+ };
@@ -0,0 +1,120 @@
1
+ /*
2
+ * Tencent is pleased to support the open source community by making TMagicEditor available.
3
+ *
4
+ * Copyright (C) 2025 Tencent. All rights reserved.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import { cloneDeep } from 'lodash-es';
20
+
21
+ import type { GroupListConfig, TableColumnConfig, TableConfig } from '../schema';
22
+
23
+ /**
24
+ * table / group-list 是同一份配置的两种展示形态,可以互相切换(`TableGroupList.vue`)。
25
+ *
26
+ * 本模块收口两种形态之间的配置派生,以及各形态下「子项配置怎么算出来」的规则,
27
+ * 供渲染(`TableGroupList.vue` / `GroupListItem.vue` / `useTableColumns.ts`)与无渲染校验
28
+ * (`collectValidatableFields`)共用,避免两条链路各写一份而产生偏差。
29
+ */
30
+
31
+ /** group-list 形态的 type(兼容驼峰与中划线两种写法) */
32
+ export const isGroupListType = (type: unknown): boolean => type === 'groupList' || type === 'group-list';
33
+
34
+ /** 按 label 文案长度估算 label 宽度(中文按 20px、其他按 8px,最小 80px) */
35
+ export const calcLabelWidth = (label: string): string => {
36
+ if (!label) return '0px';
37
+ const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
38
+ const chLength = label.length - zhLength;
39
+ return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
40
+ };
41
+
42
+ /** 由 group-list 形态的配置派生出 table 形态所需的配置;本身已是 table 形态则原样返回 */
43
+ export const toTableConfig = (config: TableConfig | GroupListConfig): TableConfig => {
44
+ if (!isGroupListType(config.type)) return config as TableConfig;
45
+
46
+ const source = config as GroupListConfig;
47
+ return {
48
+ ...config,
49
+ type: 'table',
50
+ groupItems: source.items,
51
+ items:
52
+ source.tableItems ||
53
+ (source.items as any[]).map((item: any) => ({
54
+ ...item,
55
+ label: item.label || item.text,
56
+ text: null,
57
+ })),
58
+ } as any as TableConfig;
59
+ };
60
+
61
+ /** 由 table 形态的配置派生出 group-list 形态所需的配置;本身已是 group-list 形态则原样返回 */
62
+ export const toGroupListConfig = (config: TableConfig | GroupListConfig): GroupListConfig => {
63
+ if (isGroupListType(config.type)) return config as GroupListConfig;
64
+
65
+ const source = config as TableConfig;
66
+ return {
67
+ ...config,
68
+ type: 'groupList',
69
+ tableItems: source.items,
70
+ items:
71
+ source.groupItems ||
72
+ (source.items as any[]).map((item: any) => {
73
+ const text = item.text || item.label;
74
+ return {
75
+ ...item,
76
+ text,
77
+ labelWidth: calcLabelWidth(text),
78
+ span: item.span || 12,
79
+ };
80
+ }),
81
+ } as any as GroupListConfig;
82
+ };
83
+
84
+ /**
85
+ * group-list 每一行渲染的 row 容器配置(`GroupListItem.vue` 渲染的 `Container` 的 config)。
86
+ *
87
+ * `keyProp` 由调用方从 `mForm.keyProp` 取,默认 `__key`。
88
+ */
89
+ export const getGroupListRowConfig = (config: GroupListConfig, index: number, keyProp?: string) => {
90
+ const key = keyProp || '__key';
91
+
92
+ return {
93
+ type: 'row',
94
+ span: config.span || 24,
95
+ items: config.items,
96
+ labelWidth: config.labelWidth,
97
+ labelPosition: config.labelPosition,
98
+ [key]: `${(config as Record<string, any>)[key]}${String(index)}`,
99
+ };
100
+ };
101
+
102
+ /**
103
+ * 列是否会被渲染成一列(`hidden` 列不渲染,`display` 为假的列也不渲染)。
104
+ *
105
+ * `display` 的求值上下文是表格自身的 props,由调用方通过 `evalDisplay` 注入。
106
+ */
107
+ export const isTableColumnRendered = (column: TableColumnConfig, evalDisplay: (_display: any) => any): boolean =>
108
+ column.type !== 'hidden' && Boolean(evalDisplay(column.display));
109
+
110
+ /**
111
+ * 单元格内 `Container` 实际收到的列配置:按行求值 `itemsFunction`,并去掉已在列层面消费过的 `display`。
112
+ */
113
+ export const makeTableColumnConfig = (column: TableColumnConfig, row: any): TableColumnConfig => {
114
+ const newConfig = cloneDeep(column);
115
+ if (typeof column.itemsFunction === 'function') {
116
+ newConfig.items = column.itemsFunction(row);
117
+ }
118
+ delete newConfig.display;
119
+ return newConfig;
120
+ };
@@ -18,9 +18,10 @@
18
18
 
19
19
  import { readonly } from 'vue';
20
20
  import dayjs from 'dayjs';
21
- import customParseFormat from 'dayjs/plugin/customParseFormat';
21
+ // dayjs 没有 exports 映射,原生 Node ESM 不会补扩展名,深路径必须写全 .js
22
+ import customParseFormat from 'dayjs/plugin/customParseFormat.js';
22
23
 
23
- import { appendValidateSuggestion } from '@tmagic/design';
24
+ import { appendValidateSuggestion } from '@tmagic/design/headless';
24
25
  import { getValueByKeyPath, toLine } from '@tmagic/utils';
25
26
 
26
27
  import type { CascaderOption, FormState, Rule } from '../schema';
@@ -49,33 +50,36 @@ export type TypeMatchValidator = (
49
50
  ) => string | undefined | Promise<string | undefined>;
50
51
  // #endregion TypeMatchValidator
51
52
 
52
- const typeMatchRuleRegistry = new Map<string, TypeMatchValidator>();
53
+ const extraTypeMatchRules = new Map<string, TypeMatchValidator>();
54
+ const builtInTypeMatchRules = new Map<string, TypeMatchValidator>();
53
55
 
54
56
  const isPromise = (value: any): value is Promise<unknown> =>
55
57
  typeof value === 'object' && value !== null && typeof value.then === 'function';
56
58
 
57
- /** 注册或覆盖某个字段 type 的 typeMatch 校验规则 */
58
- export const registerTypeMatchRule = (type: string, validator: TypeMatchValidator): void => {
59
- typeMatchRuleRegistry.set(toLine(type), validator);
59
+ /** 注册或覆盖某个字段 type 的 typeMatch 校验规则。`builtIn` 登记不受 `clearTypeMatchRules` / `deleteTypeMatchRule` 影响。 */
60
+ export const registerTypeMatchRule = (type: string, validator: TypeMatchValidator, builtIn = false): void => {
61
+ (builtIn ? builtInTypeMatchRules : extraTypeMatchRules).set(toLine(type), validator);
60
62
  };
61
63
 
62
64
  /** 批量注册 typeMatch 校验规则 */
63
- export const registerTypeMatchRules = (rules: Record<string, TypeMatchValidator>): void => {
65
+ export const registerTypeMatchRules = (rules: Record<string, TypeMatchValidator>, builtIn = false): void => {
64
66
  Object.entries(rules).forEach(([type, validator]) => {
65
- registerTypeMatchRule(type, validator);
67
+ registerTypeMatchRule(type, validator, builtIn);
66
68
  });
67
69
  };
68
70
 
69
- /** 获取某个字段 type 的自定义 typeMatch 校验规则 */
70
- export const getTypeMatchRule = (type: string): TypeMatchValidator | undefined =>
71
- typeMatchRuleRegistry.get(toLine(type));
71
+ /** 获取某个字段 type 的 typeMatch 校验规则(业务侧优先于内置) */
72
+ export const getTypeMatchRule = (type: string): TypeMatchValidator | undefined => {
73
+ const key = toLine(type);
74
+ return extraTypeMatchRules.get(key) ?? builtInTypeMatchRules.get(key);
75
+ };
72
76
 
73
- /** 删除某个字段 type 的自定义 typeMatch 校验规则 */
74
- export const deleteTypeMatchRule = (type: string): boolean => typeMatchRuleRegistry.delete(toLine(type));
77
+ /** 删除业务侧 typeMatch 校验规则(不影响内置) */
78
+ export const deleteTypeMatchRule = (type: string): boolean => extraTypeMatchRules.delete(toLine(type));
75
79
 
76
- /** 清空所有自定义 typeMatch 校验规则 */
80
+ /** 清空业务侧 typeMatch 校验规则(不影响内置) */
77
81
  export const clearTypeMatchRules = (): void => {
78
- typeMatchRuleRegistry.clear();
82
+ extraTypeMatchRules.clear();
79
83
  };
80
84
 
81
85
  /** 本地解析配置函数,避免与 form.ts 循环依赖 */