@tmagic/form 1.8.0-beta.25 → 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 (130) 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 +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 +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 +81 -1
  63. package/dist/themes/magic-admin.css +83 -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/group-list.scss +107 -1
  107. package/src/theme/themes/magic-admin/index.scss +2 -8
  108. package/src/utils/builtInFields.ts +69 -0
  109. package/src/utils/collectFields.ts +561 -0
  110. package/src/utils/config.ts +1 -15
  111. package/src/utils/fieldInnerConfig.ts +127 -0
  112. package/src/utils/fieldValueEffects.ts +140 -0
  113. package/src/utils/form.ts +141 -75
  114. package/src/utils/formStateProxy.ts +104 -0
  115. package/src/utils/registerField.ts +308 -0
  116. package/src/utils/submitHeadless.ts +206 -0
  117. package/src/utils/tableGroupList.ts +120 -0
  118. package/src/utils/typeMatch.ts +19 -15
  119. package/src/utils/validateError.ts +92 -0
  120. package/src/utils/validateValues.ts +177 -0
  121. package/types/headless.d.ts +682 -0
  122. package/types/index.d.ts +631 -272
  123. package/dist/es/fields/CheckboxGroup.js +0 -5
  124. package/dist/es/fields/Date.js +0 -5
  125. package/dist/es/fields/DateTime.js +0 -5
  126. package/dist/es/fields/Display.js +0 -5
  127. package/dist/es/fields/DynamicField.js +0 -5
  128. package/dist/es/fields/NumberRange.js +0 -5
  129. package/dist/es/utils/silentLeafFieldTypes.js +0 -49
  130. package/src/utils/silentLeafFieldTypes.ts +0 -71
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.25",
2
+ "version": "1.8.0-beta.26",
3
3
  "name": "@tmagic/form",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -17,6 +17,11 @@
17
17
  "import": "./dist/es/index.js",
18
18
  "require": "./dist/tmagic-form.umd.cjs"
19
19
  },
20
+ "./headless": {
21
+ "types": "./types/headless.d.ts",
22
+ "import": "./dist/es/headless.js",
23
+ "require": "./dist/tmagic-form-headless.umd.cjs"
24
+ },
20
25
  "./dist/style.css": {
21
26
  "import": "./dist/style.css",
22
27
  "require": "./dist/style.css"
@@ -40,6 +45,7 @@
40
45
  "dependencies": {
41
46
  "@element-plus/icons-vue": "^2.3.2",
42
47
  "@popperjs/core": "^2.11.8",
48
+ "async-validator": "^4.2.5",
43
49
  "dayjs": "^1.11.21",
44
50
  "lodash-es": "^4.18.1",
45
51
  "sortablejs": "^1.15.7"
@@ -52,9 +58,9 @@
52
58
  "peerDependencies": {
53
59
  "typescript": "^6.0.3",
54
60
  "vue": "^3.5.41",
55
- "@tmagic/form-schema": "1.8.0-beta.25",
56
- "@tmagic/design": "1.8.0-beta.25",
57
- "@tmagic/utils": "1.8.0-beta.25"
61
+ "@tmagic/design": "1.8.0-beta.26",
62
+ "@tmagic/utils": "1.8.0-beta.26",
63
+ "@tmagic/form-schema": "1.8.0-beta.26"
58
64
  },
59
65
  "peerDependenciesMeta": {
60
66
  "typescript": {
package/src/Form.vue CHANGED
@@ -23,6 +23,7 @@
23
23
  :last-values="lastValuesProcessed"
24
24
  :is-compare="isCompare"
25
25
  :label-width="item.labelWidth || labelWidth"
26
+ :label-position="item.labelPosition || labelPosition"
26
27
  :step-active="stepActive"
27
28
  :size="size"
28
29
  @change="changeHandler"
@@ -36,37 +37,26 @@
36
37
  </template>
37
38
 
38
39
  <script setup lang="ts">
39
- import {
40
- computed,
41
- inject,
42
- nextTick,
43
- provide,
44
- reactive,
45
- ref,
46
- shallowRef,
47
- toRaw,
48
- useTemplateRef,
49
- watch,
50
- watchEffect,
51
- } from 'vue';
52
- import { cloneDeep, isEqual } from 'lodash-es';
40
+ import { computed, inject, nextTick, provide, reactive, ref, shallowRef, toRaw, useTemplateRef, watch } from 'vue';
41
+ import { cloneDeep, isEqualWith } from 'lodash-es';
53
42
 
54
43
  import { M_THEME_KEY, TMagicForm, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
55
44
  import { setValueByKeyPath } from '@tmagic/utils';
56
45
 
57
46
  import Container from './containers/Container.vue';
58
- import { getConfig } from './utils/config';
59
- import { applyExtendState, initValue } from './utils/form';
47
+ import { applyMountValueEffects } from './utils/collectFields';
48
+ import { createFormStateBase, createFormStateProxy, initValue, mergeFormContexts } from './utils/form';
49
+ import { formatValidateError as formatError, getTextByName as findTextByName } from './utils/validateError';
60
50
  import type {
61
51
  ChangeRecord,
62
52
  ContainerChangeEventData,
63
53
  FormConfig,
54
+ FormContext,
64
55
  FormSlots,
65
56
  FormState,
66
57
  FormValue,
67
- ValidateError,
68
58
  } from './schema';
69
- import { FORM_DIFF_CONFIG_KEY, FORM_TYPE_MATCH_VALID_KEY } from './schema';
59
+ import { FORM_CONTEXT_KEY, FORM_DIFF_CONFIG_KEY, FORM_TYPE_MATCH_VALID_KEY } from './schema';
70
60
 
71
61
  defineOptions({
72
62
  name: 'MForm',
@@ -100,7 +90,7 @@ const props = withDefaults(
100
90
  stepActive?: string | number;
101
91
  size?: 'small' | 'default' | 'large';
102
92
  inline?: boolean;
103
- labelPosition?: string;
93
+ labelPosition?: 'top' | 'left' | 'right';
104
94
  keyProp?: string;
105
95
  popperClass?: string;
106
96
  preventSubmitDefault?: boolean;
@@ -111,7 +101,12 @@ const props = withDefaults(
111
101
  * - `false`:跳过查找,直接使用字段 name 作为错误提示前缀(形如 `字段name -> 错误信息`)。
112
102
  */
113
103
  useFieldTextInError?: boolean;
114
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
104
+ /**
105
+ * 宿主业务上下文。也可由祖先 `provide(FORM_CONTEXT_KEY)` 下发,本 prop 覆盖祖先的同名字段。
106
+ *
107
+ * 配置回调通过 `mForm.xxx` 读取,由 formState 的读穿 Proxy 落到这里。
108
+ */
109
+ context?: FormContext;
115
110
  /**
116
111
  * 自定义"是否展示对比内容"的判断函数(仅在 `isCompare === true` 时生效)。
117
112
  *
@@ -178,9 +173,6 @@ const tMagicFormRef = useTemplateRef('tMagicForm');
178
173
  const initialized = ref(false);
179
174
  const values = ref<FormValue>({});
180
175
  const lastValuesProcessed = ref<FormValue>({});
181
- const fields = new Map<string, any>();
182
-
183
- const requestFuc = getConfig('request') as Function;
184
176
 
185
177
  /**
186
178
  * 当前表单生效的主题名称:
@@ -213,18 +205,14 @@ const themeClass = computed(() => (effectiveTheme.value ? `m-theme--${effectiveT
213
205
  * 2. `values` / `lastValuesProcessed` 是 ref,Vue 的 `reactive` 会自动解包,因此每次
214
206
  * 访问 `formState.values` / `formState.lastValuesProcessed` 也都是当前 ref 值。
215
207
  *
216
- * 3. `extendState` 注入的字段在下方的 `watchEffect` 中合并到 `formState`:
217
- * - data 描述符(普通字段)通过 `formState[key] = value` 写入,走 reactive proxy 的
218
- * set,触发依赖通知;`extendState` 同步段读到的响应式数据变化时会自动重跑,
219
- * 把最新值刷进 formState。
220
- * - accessor 描述符(`{ get stage() { return ... } }`)按原样写入,调用方可以控制
221
- * 读时求值,每次读取都会重新执行 getter。
208
+ * 3. 宿主业务上下文不再 merge 进 coreState,而是单独放在 `contextRef` 上,由读穿
209
+ * Proxy 在 miss 时落到 context。核心字段结构性优先,`mForm.xxx` 永久兼容旧读法。
222
210
  *
223
211
  * 4. `popperClass` 会自动拼接 `themeClass`:调用方传入的 `popperClass` + 当前主题
224
212
  * 修饰类(含祖先 `<MEditor>` provide 的主题)。这样所有透传到 Element Plus 弹层
225
213
  * `popper-class` 的字段(Select / DateTime / Cascader 等)能自带主题作用域。
226
214
  */
227
- const formState: FormState = reactive<FormState>({
215
+ const coreState: FormState = reactive<FormState>({
228
216
  get keyProp() {
229
217
  return props.keyProp;
230
218
  },
@@ -253,76 +241,21 @@ const formState: FormState = reactive<FormState>({
253
241
  values,
254
242
  lastValuesProcessed,
255
243
  $emit: emit as (_event: string, ..._args: any[]) => void,
256
- fields,
257
- setField: (prop: string, field: any) => fields.set(prop, field),
258
- getField: (prop: string) => fields.get(prop),
259
- deleteField: (prop: string) => fields.delete(prop),
260
- $messageBox: tMagicMessageBox,
261
- $message: tMagicMessage,
262
- post: (options: any) => {
263
- if (requestFuc) {
264
- return requestFuc({
265
- method: 'POST',
266
- ...options,
267
- });
268
- }
269
- },
244
+ ...createFormStateBase({ $message: tMagicMessage, $messageBox: tMagicMessageBox }),
270
245
  });
271
246
 
272
- /**
273
- * formState 的内置 key 快照(keyProp / values / $emit / fields / post 等)。
274
- *
275
- * 在 `extendState` 首次合并前捕获,`applyExtendState` 会据此禁止 `extendState`
276
- * 覆盖这些已有字段(只能新增字段),避免表单核心状态被外部意外改写。
277
- *
278
- * 之所以在此处(effect 之外)捕获而不是在 `applyExtendState` 内动态取:
279
- * `watchEffect` 会在依赖变化时重跑,若动态取,`extendState` 自己新增的字段在第二次
280
- * 合并时也会被当成「已有 key」而拒绝刷新;这里只锁定内置字段即可规避该问题。
281
- */
282
- const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(formState));
247
+ const ancestorContext = inject(FORM_CONTEXT_KEY, undefined);
283
248
 
284
249
  /**
285
- * `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
286
- * 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
287
- *
288
- * extendState: (formState) => ({
289
- * username: store.username, // 同步读 store,会被跟踪
290
- * env: store.env,
291
- * })
292
- *
293
- * 当 `store.username` 变化时,整个 effect 重跑,新值会被刷进 `formState`。
294
- *
295
- * prop 派生字段(initValues / config / ...)已经在上方用 getter 定义,
296
- * 这里不再重复同步;因此 `props.initValues` 这类高频变化也不会再触发
297
- * `extendState` 重跑(旧版的性能问题修复点)。
298
- *
299
- * 实现细节:合并逻辑统一收口在 `applyExtendState`(utils/form)——
300
- * data 描述符走 reactive proxy 的 set 触发依赖通知(与旧版「逐项赋值」语义等价),
301
- * accessor 描述符按原样 defineProperty 支持读时求值;
302
- * props 派生的只读 getter 字段(keyProp 等)以普通字段形式返回时会被跳过并告警。
250
+ * 宿主业务上下文:`props.context` 覆盖祖先注入的同名字段。
251
+ * 嵌套表单(Link / FormBox / FormDialog)通过下面的 provide 自动继承。
303
252
  */
304
- watchEffect(async (onCleanup) => {
305
- const { extendState } = props;
306
- if (typeof extendState !== 'function') return;
307
-
308
- let stale = false;
309
- onCleanup(() => {
310
- stale = true;
311
- });
312
-
313
- let state: Record<string, any> = {};
314
- try {
315
- state = (await extendState(formState)) || {};
316
- } catch (e) {
317
- console.error('[MForm] extendState failed:', e);
318
- return;
319
- }
320
- if (stale) return;
253
+ const contextRef = computed<FormContext>(() => mergeFormContexts(ancestorContext?.value, props.context));
321
254
 
322
- applyExtendState(formState, state, reservedStateKeys);
323
- });
255
+ const formState: FormState = createFormStateProxy(coreState, () => contextRef.value);
324
256
 
325
257
  provide('mForm', formState);
258
+ provide(FORM_CONTEXT_KEY, contextRef);
326
259
 
327
260
  /**
328
261
  * 把生效主题(自身或祖先)再 provide 出去,供 form 子树内含 `Teleport` 的组件
@@ -344,12 +277,23 @@ provide(FORM_DIFF_CONFIG_KEY, {
344
277
 
345
278
  const changeRecords = shallowRef<ChangeRecord[]>([]);
346
279
 
280
+ /**
281
+ * 两份配置的结构是否一致;函数一律视为相等。
282
+ *
283
+ * 宿主(如编辑器属性面板)往往在每次节点更新后整份重新生成配置,其中的
284
+ * `display` / `options` / `onChange` 都是新闭包,深比较必然判不等。若据此把 `initialized`
285
+ * 置 false,整棵表单会卸载重挂,滚动位置、展开态、输入焦点全部丢失。
286
+ * 配置是响应式 prop,闭包换了照样生效,只有结构变化(增删字段、换组件类型)才需要重挂。
287
+ */
288
+ const isSameConfigShape = (config: unknown, preConfig: unknown) =>
289
+ isEqualWith(config, preConfig, (a, b) => (typeof a === 'function' && typeof b === 'function' ? true : undefined));
290
+
347
291
  watch(
348
292
  [() => props.config, () => props.initValues],
349
- ([config], [preConfig]) => {
293
+ async ([config], [preConfig]) => {
350
294
  changeRecords.value = [];
351
295
 
352
- if (!isEqual(toRaw(config), toRaw(preConfig))) {
296
+ if (!isSameConfigShape(toRaw(config), toRaw(preConfig))) {
353
297
  initialized.value = false;
354
298
  }
355
299
 
@@ -358,6 +302,9 @@ watch(
358
302
  config: props.config,
359
303
  }).then((value) => {
360
304
  values.value = value;
305
+ // 字段的值初始化写入统一在这里执行,字段组件自身不再改写 model。
306
+ // 必须放在赋值之后:effect 与动态 type / display 回调读到的 formValue 才是这一轮的值。
307
+ applyMountValueEffects(formState, props.config, values.value);
361
308
  // 非对比模式,初始化完成
362
309
  initialized.value = !props.isCompare;
363
310
 
@@ -375,6 +322,8 @@ watch(
375
322
  config: props.config,
376
323
  }).then((value) => {
377
324
  lastValuesProcessed.value = value;
325
+ // 对比模式下待对比的那份值同样要规整,否则会与当前值比出「格式差异」这种假差异
326
+ applyMountValueEffects(formState, props.config, lastValuesProcessed.value);
378
327
  initialized.value = true;
379
328
  });
380
329
  }
@@ -406,66 +355,17 @@ const submitHandler = (e: SubmitEvent) => {
406
355
  }
407
356
  };
408
357
 
409
- /**
410
- * 通过 name 从 config 中查找对应的 text
411
- * @param name - 字段名,支持点分隔的路径格式,如 'a.b.c'
412
- * @param config - 表单配置数组
413
- * @returns 找到的 text 值,如果未找到则返回 undefined
414
- */
415
- const getTextByName = (name: string, config: FormConfig = props.config): string | undefined => {
416
- if (!name || !Array.isArray(config)) return undefined;
417
-
418
- const nameParts = name.split('.');
419
-
420
- const findInConfig = (configs: FormConfig, parts: string[]): string | undefined => {
421
- if (parts.length === 0) return undefined;
422
-
423
- const [currentPart, ...remainingParts] = parts;
424
-
425
- for (const item of configs) {
426
- if (item.name === currentPart) {
427
- if (remainingParts.length === 0) {
428
- return typeof item.text === 'string' ? item.text : undefined;
429
- }
430
-
431
- if ('items' in item && Array.isArray(item.items)) {
432
- const result = findInConfig(item.items, remainingParts);
433
- if (result !== undefined) return result;
434
- }
435
- }
436
-
437
- if ('items' in item && Array.isArray(item.items)) {
438
- const result = findInConfig(item.items, parts);
439
- if (result !== undefined) return result;
440
- }
441
- }
442
-
443
- return undefined;
444
- };
445
-
446
- return findInConfig(config, nameParts);
447
- };
358
+ const getTextByName = (name: string, config: FormConfig = props.config): string | undefined =>
359
+ findTextByName(name, config);
448
360
 
449
361
  /**
450
362
  * 将校验返回的 invalidFields 汇总为可读的错误文案(多条以 `<br>` 拼接)。
451
363
  *
452
- * 抽离为独立方法,供 `submitForm`(提交校验)与 `validate`(返回错误文案的校验)复用,
453
- * 保证两种校验入口产出的错误文案格式完全一致。
364
+ * 实现收口在 `utils/validateError`,供渲染式校验(本组件的 `submitForm` / `validate`)与
365
+ * 无渲染校验(`validateValues`)共用,保证两条链路产出的错误文案格式完全一致。
454
366
  */
455
- const formatValidateError = (invalidFields: Record<string, any>): string => {
456
- const error: string[] = [];
457
-
458
- Object.entries(invalidFields).forEach(([prop, validateError]) => {
459
- (validateError as ValidateError[]).forEach(({ field, message }) => {
460
- const name = field || prop;
461
- const text = (props.useFieldTextInError ? getTextByName(name, props.config) : undefined) || name;
462
-
463
- error.push(`${text} -> ${message}`);
464
- });
465
- });
466
-
467
- return error.join('<br>');
468
- };
367
+ const formatValidateError = (invalidFields: Record<string, any>): string =>
368
+ formatError(invalidFields, { config: props.config, useFieldTextInError: props.useFieldTextInError });
469
369
 
470
370
  defineExpose({
471
371
  values,
package/src/FormBox.vue CHANGED
@@ -14,7 +14,7 @@
14
14
  :inline="inline"
15
15
  :prevent-submit-default="preventSubmitDefault"
16
16
  :use-field-text-in-error="useFieldTextInError"
17
- :extend-state="extendState"
17
+ :context="context"
18
18
  :type-match-valid="typeMatchValid"
19
19
  :validate-on-init="validateOnInit"
20
20
  @change="changeHandler"
@@ -44,7 +44,7 @@ import { computed, ref, watchEffect } from 'vue';
44
44
  import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
45
45
 
46
46
  import Form from './Form.vue';
47
- import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from './schema';
47
+ import type { ContainerChangeEventData, FormConfig, FormContext, FormValue } from './schema';
48
48
 
49
49
  defineOptions({
50
50
  name: 'MFormBox',
@@ -66,11 +66,12 @@ const props = withDefaults(
66
66
  size?: 'small' | 'default' | 'large';
67
67
  confirmText?: string;
68
68
  inline?: boolean;
69
- labelPosition?: string;
69
+ labelPosition?: 'top' | 'left' | 'right';
70
70
  preventSubmitDefault?: boolean;
71
71
  /** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
72
72
  useFieldTextInError?: boolean;
73
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
73
+ /** 透传给内部 `MForm` 的宿主业务上下文 */
74
+ context?: FormContext;
74
75
  }>(),
75
76
  {
76
77
  config: () => [],
@@ -34,7 +34,7 @@
34
34
  :use-field-text-in-error="useFieldTextInError"
35
35
  :type-match-valid="typeMatchValid"
36
36
  :validate-on-init="validateOnInit"
37
- :extend-state="extendState"
37
+ :context="context"
38
38
  :theme="effectiveTheme"
39
39
  @change="changeHandler"
40
40
  ></Form>
@@ -73,7 +73,7 @@ import { computed, inject, provide, ref } from 'vue';
73
73
  import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDialog, TMagicRow } from '@tmagic/design';
74
74
 
75
75
  import Form from './Form.vue';
76
- import { ContainerChangeEventData, FormConfig, FormState, FormValue, StepConfig } from './schema';
76
+ import { ContainerChangeEventData, FormConfig, FormContext, FormValue, StepConfig } from './schema';
77
77
 
78
78
  defineOptions({
79
79
  name: 'MFormDialog',
@@ -94,7 +94,7 @@ const props = withDefaults(
94
94
  disabled?: boolean;
95
95
  title?: string;
96
96
  inline?: boolean;
97
- labelPosition?: string;
97
+ labelPosition?: 'top' | 'left' | 'right';
98
98
  zIndex?: number;
99
99
  size?: 'small' | 'default' | 'large';
100
100
  confirmText?: string;
@@ -106,8 +106,8 @@ const props = withDefaults(
106
106
  showCancel?: boolean;
107
107
  /** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
108
108
  useFieldTextInError?: boolean;
109
- /** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
110
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
109
+ /** 透传给内部 `MForm` 的宿主业务上下文 */
110
+ context?: FormContext;
111
111
  /**
112
112
  * 主题名。优先级:传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串。
113
113
  * 计算结果会再次 `provide` 出去,使得 Dialog 被 Teleport 到 body 后,内部子树
@@ -31,7 +31,7 @@
31
31
  :use-field-text-in-error="useFieldTextInError"
32
32
  :type-match-valid="typeMatchValid"
33
33
  :validate-on-init="validateOnInit"
34
- :extend-state="extendState"
34
+ :context="context"
35
35
  :theme="effectiveTheme"
36
36
  @change="changeHandler"
37
37
  ></Form>
@@ -64,7 +64,7 @@ import { computed, inject, provide, ref, watchEffect } from 'vue';
64
64
  import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDrawer, TMagicRow } from '@tmagic/design';
65
65
 
66
66
  import Form from './Form.vue';
67
- import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from './schema';
67
+ import type { ContainerChangeEventData, FormConfig, FormContext, FormValue } from './schema';
68
68
 
69
69
  defineOptions({
70
70
  name: 'MFormDrawer',
@@ -88,14 +88,14 @@ const props = withDefaults(
88
88
  size?: 'small' | 'default' | 'large';
89
89
  confirmText?: string;
90
90
  inline?: boolean;
91
- labelPosition?: string;
91
+ labelPosition?: 'top' | 'left' | 'right';
92
92
  preventSubmitDefault?: boolean;
93
93
  /** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
94
94
  useFieldTextInError?: boolean;
95
95
  /** 关闭前的回调,会暂停 Drawer 的关闭; done 是个 function type 接受一个 boolean 参数, 执行 done 使用 true 参数或不提供参数将会终止关闭 */
96
96
  beforeClose?: (_done: (_cancel?: boolean) => void) => void;
97
- /** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
98
- extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
97
+ /** 透传给内部 `MForm` 的宿主业务上下文 */
98
+ context?: FormContext;
99
99
  /**
100
100
  * 主题名。优先级:传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串。
101
101
  * 计算结果会再次 `provide` 出去,使得 Drawer 被 Teleport 到 body 后,内部子树
@@ -7,6 +7,7 @@
7
7
  :config="config"
8
8
  :prop="prop"
9
9
  :label-width="config.labelWidth || labelWidth"
10
+ :label-position="config.labelPosition || labelPosition"
10
11
  :expand-more="expandMore"
11
12
  :size="size"
12
13
  :disabled="disabled"
@@ -37,6 +38,7 @@ const props = defineProps<{
37
38
  isCompare?: boolean;
38
39
  config: FormItemConfig;
39
40
  labelWidth?: string | number;
41
+ labelPosition?: 'top' | 'left' | 'right';
40
42
  expandMore?: boolean;
41
43
  span?: number;
42
44
  size?: string;