@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
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.2",
2
+ "version": "1.8.0-beta.21",
3
3
  "name": "@tmagic/form",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -50,11 +50,11 @@
50
50
  "@vue/test-utils": "^2.4.6"
51
51
  },
52
52
  "peerDependencies": {
53
- "vue": "^3.5.34",
53
+ "vue": "^3.5.40",
54
54
  "typescript": "^6.0.3",
55
- "@tmagic/design": "1.8.0-beta.2",
56
- "@tmagic/form-schema": "1.8.0-beta.2",
57
- "@tmagic/utils": "1.8.0-beta.2"
55
+ "@tmagic/design": "1.8.0-beta.21",
56
+ "@tmagic/utils": "1.8.0-beta.21",
57
+ "@tmagic/form-schema": "1.8.0-beta.21"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "typescript": {
package/src/Form.vue CHANGED
@@ -1,6 +1,10 @@
1
1
  <template>
2
2
  <TMagicForm
3
- class="m-form"
3
+ :class="[
4
+ 'm-form',
5
+ effectiveTheme ? `m-form--${effectiveTheme}` : '',
6
+ effectiveTheme ? `m-theme--${effectiveTheme}` : '',
7
+ ]"
4
8
  ref="tMagicForm"
5
9
  :model="values"
6
10
  :label-width="labelWidth"
@@ -32,15 +36,27 @@
32
36
  </template>
33
37
 
34
38
  <script setup lang="ts">
35
- import { provide, reactive, ref, shallowRef, toRaw, useTemplateRef, watch, watchEffect } from 'vue';
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';
36
52
  import { cloneDeep, isEqual } from 'lodash-es';
37
53
 
38
- import { TMagicForm, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
54
+ import { M_THEME_KEY, TMagicForm, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
39
55
  import { setValueByKeyPath } from '@tmagic/utils';
40
56
 
41
57
  import Container from './containers/Container.vue';
42
58
  import { getConfig } from './utils/config';
43
- import { initValue } from './utils/form';
59
+ import { applyExtendState, initValue } from './utils/form';
44
60
  import type {
45
61
  ChangeRecord,
46
62
  ContainerChangeEventData,
@@ -50,7 +66,7 @@ import type {
50
66
  FormValue,
51
67
  ValidateError,
52
68
  } from './schema';
53
- import { FORM_DIFF_CONFIG_KEY } from './schema';
69
+ import { FORM_DIFF_CONFIG_KEY, FORM_TYPE_MATCH_VALID_KEY } from './schema';
54
70
 
55
71
  defineOptions({
56
72
  name: 'MForm',
@@ -70,6 +86,15 @@ const props = withDefaults(
70
86
  isCompare?: boolean;
71
87
  parentValues?: Record<string, any>;
72
88
  labelWidth?: string;
89
+ /** 是否开启类型匹配校验 */
90
+ typeMatchValid?: boolean;
91
+ /**
92
+ * 初始化(`config` / `initValues` 就绪)后是否立即执行一次表单校验。
93
+ *
94
+ * - `false`(默认):不自动校验,避免打开表单时就展示错误态;
95
+ * - `true`:初始化完成后在 `nextTick` 中调用 `validate()`。
96
+ */
97
+ validateOnInit?: boolean;
73
98
  disabled?: boolean;
74
99
  height?: string;
75
100
  stepActive?: string | number;
@@ -79,6 +104,13 @@ const props = withDefaults(
79
104
  keyProp?: string;
80
105
  popperClass?: string;
81
106
  preventSubmitDefault?: boolean;
107
+ /**
108
+ * 表单校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
109
+ *
110
+ * - `true`(默认):错误提示形如 `字段文案 -> 错误信息`,找不到 text 时回退为字段 name;
111
+ * - `false`:跳过查找,直接使用字段 name 作为错误提示前缀(形如 `字段name -> 错误信息`)。
112
+ */
113
+ useFieldTextInError?: boolean;
82
114
  extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
83
115
  /**
84
116
  * 自定义"是否展示对比内容"的判断函数(仅在 `isCompare === true` 时生效)。
@@ -107,6 +139,15 @@ const props = withDefaults(
107
139
  * 通过 provide 下发,对整棵表单的所有层级 Container 生效,只需在 MForm 这一层传一次。
108
140
  */
109
141
  selfDiffFieldTypes?: string[] | ((_defaultTypes: string[]) => string[]);
142
+ /**
143
+ * 主题名称:对应 `packages/form/src/theme/themes/<theme>/index.scss` 的目录名。
144
+ *
145
+ * 设置后会在表单根元素上追加 `m-form--<theme>` 修饰类,配合按需引入
146
+ * `@tmagic/form/dist/themes/<theme>.css` 即可启用主题样式。
147
+ *
148
+ * 例如:`<MForm theme="magic-admin" />` + `import '@tmagic/form/dist/themes/magic-admin.css'`。
149
+ */
150
+ theme?: string;
110
151
  }>(),
111
152
  {
112
153
  config: () => [],
@@ -121,11 +162,18 @@ const props = withDefaults(
121
162
  inline: false,
122
163
  labelPosition: 'right',
123
164
  keyProp: '__key',
165
+ useFieldTextInError: true,
166
+ validateOnInit: false,
124
167
  },
125
168
  );
126
169
 
127
170
  const emit = defineEmits(['change', 'error', 'field-input', 'field-change', 'update:stepActive']);
128
171
 
172
+ provide(
173
+ FORM_TYPE_MATCH_VALID_KEY,
174
+ computed(() => props.typeMatchValid),
175
+ );
176
+
129
177
  const tMagicFormRef = useTemplateRef('tMagicForm');
130
178
  const initialized = ref(false);
131
179
  const values = ref<FormValue>({});
@@ -134,6 +182,27 @@ const fields = new Map<string, any>();
134
182
 
135
183
  const requestFuc = getConfig('request') as Function;
136
184
 
185
+ /**
186
+ * 当前表单生效的主题名称:
187
+ * - 优先用本组件自己的 `props.theme`;
188
+ * - 没设置时回退到最近祖先 `<MEditor>` / `<MForm>` provide 的主题,便于内嵌于编辑器
189
+ * 时自动跟随外层主题,无需在每个 `MForm` 上重复传 `theme`。
190
+ *
191
+ * 同时把合并后的值再 provide 出去(见下方 `provide(M_THEME_KEY, ...)`),让 form 子树
192
+ * 里再嵌套的 portal 组件(`TMagicPopover` 等)依然能拿到非空主题。
193
+ */
194
+ const ancestorTheme = inject(M_THEME_KEY, null);
195
+ const effectiveTheme = computed(() => props.theme || ancestorTheme?.value || '');
196
+
197
+ /**
198
+ * 拼到 `formState.popperClass` 上的主题修饰类(仅 `m-theme--<theme>`,
199
+ * 不带 `m-form` / `m-editor` 前缀,因为 Element Plus 弹层节点本身既不是 form 也不是 editor)。
200
+ *
201
+ * 这条类会随所有读 `mForm.popperClass` 的字段(Select / DateTime / Cascader 等)下发到
202
+ * Element Plus 的 `popper-class`,让 portal 节点也命中 `m-theme--<theme>` 上的 CSS 变量。
203
+ */
204
+ const themeClass = computed(() => (effectiveTheme.value ? `m-theme--${effectiveTheme.value}` : ''));
205
+
137
206
  /**
138
207
  * formState 实现说明:
139
208
  *
@@ -150,13 +219,21 @@ const requestFuc = getConfig('request') as Function;
150
219
  * 把最新值刷进 formState。
151
220
  * - accessor 描述符(`{ get stage() { return ... } }`)按原样写入,调用方可以控制
152
221
  * 读时求值,每次读取都会重新执行 getter。
222
+ *
223
+ * 4. `popperClass` 会自动拼接 `themeClass`:调用方传入的 `popperClass` + 当前主题
224
+ * 修饰类(含祖先 `<MEditor>` provide 的主题)。这样所有透传到 Element Plus 弹层
225
+ * `popper-class` 的字段(Select / DateTime / Cascader 等)能自带主题作用域。
153
226
  */
154
227
  const formState: FormState = reactive<FormState>({
155
228
  get keyProp() {
156
229
  return props.keyProp;
157
230
  },
158
231
  get popperClass() {
159
- return props.popperClass;
232
+ const userClass = props.popperClass ?? '';
233
+ const tc = themeClass.value;
234
+ if (!userClass) return tc;
235
+ if (!tc) return userClass;
236
+ return `${userClass} ${tc}`;
160
237
  },
161
238
  get config() {
162
239
  return props.config;
@@ -192,6 +269,18 @@ const formState: FormState = reactive<FormState>({
192
269
  },
193
270
  });
194
271
 
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));
283
+
195
284
  /**
196
285
  * `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
197
286
  * 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
@@ -207,11 +296,10 @@ const formState: FormState = reactive<FormState>({
207
296
  * 这里不再重复同步;因此 `props.initValues` 这类高频变化也不会再触发
208
297
  * `extendState` 重跑(旧版的性能问题修复点)。
209
298
  *
210
- * 实现细节:
211
- * - data 描述符:通过 `formState[key] = value` 走 reactive proxy 的 set
212
- * 触发依赖通知;与旧版「逐项赋值」语义完全等价。
213
- * - accessor 描述符(`{ get stage() {...} }`)按原样写入 formState,调用方
214
- * 可以自行控制读时求值;强制 `configurable: true` 以便下一次重跑可再 define。
299
+ * 实现细节:合并逻辑统一收口在 `applyExtendState`(utils/form)——
300
+ * data 描述符走 reactive proxy 的 set 触发依赖通知(与旧版「逐项赋值」语义等价),
301
+ * accessor 描述符按原样 defineProperty 支持读时求值;
302
+ * props 派生的只读 getter 字段(keyProp 等)以普通字段形式返回时会被跳过并告警。
215
303
  */
216
304
  watchEffect(async (onCleanup) => {
217
305
  const { extendState } = props;
@@ -231,18 +319,18 @@ watchEffect(async (onCleanup) => {
231
319
  }
232
320
  if (stale) return;
233
321
 
234
- for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
235
- if ('value' in descriptor) {
236
- (formState as any)[key] = (state as any)[key];
237
- } else {
238
- descriptor.configurable = true;
239
- Object.defineProperty(formState, key, descriptor);
240
- }
241
- }
322
+ applyExtendState(formState, state, reservedStateKeys);
242
323
  });
243
324
 
244
325
  provide('mForm', formState);
245
326
 
327
+ /**
328
+ * 把生效主题(自身或祖先)再 provide 出去,供 form 子树内含 `Teleport` 的组件
329
+ * (如 `TMagicPopover`)在传送目标上挂 `m-theme--<theme>` 类。
330
+ * 详见 `@tmagic/design/theme.ts`。
331
+ */
332
+ provide(M_THEME_KEY, effectiveTheme);
333
+ provide('formInline', props.inline);
246
334
  // 对比相关配置单独通过 provide 下发,所有层级的 Container 通过 inject 获取,无需逐层透传 prop。
247
335
  // 用 getter 对象保证读取时回到最新的 props 值,维持响应式。
248
336
  provide(FORM_DIFF_CONFIG_KEY, {
@@ -272,6 +360,12 @@ watch(
272
360
  values.value = value;
273
361
  // 非对比模式,初始化完成
274
362
  initialized.value = !props.isCompare;
363
+
364
+ if (props.validateOnInit) {
365
+ nextTick(() => {
366
+ tMagicFormRef.value?.validate();
367
+ });
368
+ }
275
369
  });
276
370
 
277
371
  if (props.isCompare) {
@@ -352,6 +446,27 @@ const getTextByName = (name: string, config: FormConfig = props.config): string
352
446
  return findInConfig(config, nameParts);
353
447
  };
354
448
 
449
+ /**
450
+ * 将校验返回的 invalidFields 汇总为可读的错误文案(多条以 `<br>` 拼接)。
451
+ *
452
+ * 抽离为独立方法,供 `submitForm`(提交校验)与 `validate`(返回错误文案的校验)复用,
453
+ * 保证两种校验入口产出的错误文案格式完全一致。
454
+ */
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
+ };
469
+
355
470
  defineExpose({
356
471
  values,
357
472
  lastValuesProcessed,
@@ -379,18 +494,36 @@ defineExpose({
379
494
  } catch (invalidFields: any) {
380
495
  emit('error', invalidFields);
381
496
 
382
- const error: string[] = [];
383
-
384
- Object.entries(invalidFields).forEach(([prop, ValidateError]) => {
385
- (ValidateError as ValidateError[]).forEach(({ field, message }) => {
386
- const name = field || prop;
387
- const text = getTextByName(name, props.config) || name;
388
-
389
- error.push(`${text} -> ${message}`);
390
- });
391
- });
497
+ throw new Error(formatValidateError(invalidFields));
498
+ }
499
+ },
392
500
 
393
- throw new Error(error.join('<br>'));
501
+ /**
502
+ * 校验:对表单当前值执行校验,返回汇总后的错误文案。
503
+ *
504
+ * 与 `submitForm` 的区别:
505
+ * - 校验失败时不抛异常、不触发 `error` 事件,而是以返回值形式给出错误文案;
506
+ * - 不重置 `changeRecords`,不改变提交语义,仅用于「探测」当前配置是否合法。
507
+ *
508
+ * 注意:本方法只改变「校验结果的返回方式」,并不负责「不污染页面表单状态」——
509
+ * 若需对一份独立的「配置 + 值」做完全不影响页面上已渲染表单的校验,请使用 `validateForm`
510
+ * (内部会新建一个隐藏的 MForm 实例,通过 `initValues` 传入待校验值,用完即卸载)。
511
+ *
512
+ * 典型用途:作为 `validateForm` 内部复用的校验实现;也可在已渲染的表单实例上主动调用,
513
+ * 根据返回的错误文案自行决定后续处理(如记录节点错误状态)。
514
+ *
515
+ * @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
516
+ */
517
+ validate: async (): Promise<string> => {
518
+ try {
519
+ const result = await tMagicFormRef.value?.validate();
520
+ // tdesign 通过返回值返回校验结果,element-plus 通过 throw error
521
+ if (result !== true) {
522
+ throw result;
523
+ }
524
+ return '';
525
+ } catch (invalidFields: any) {
526
+ return formatValidateError(invalidFields);
394
527
  }
395
528
  },
396
529
 
package/src/FormBox.vue CHANGED
@@ -13,7 +13,10 @@
13
13
  :label-position="labelPosition"
14
14
  :inline="inline"
15
15
  :prevent-submit-default="preventSubmitDefault"
16
+ :use-field-text-in-error="useFieldTextInError"
16
17
  :extend-state="extendState"
18
+ :type-match-valid="typeMatchValid"
19
+ :validate-on-init="validateOnInit"
17
20
  @change="changeHandler"
18
21
  ></Form>
19
22
  <slot></slot>
@@ -55,18 +58,26 @@ const props = withDefaults(
55
58
  width?: number;
56
59
  height?: number;
57
60
  labelWidth?: string;
61
+ /** 是否开启类型匹配校验 */
62
+ typeMatchValid?: boolean;
63
+ /** 透传给内部 `MForm`,初始化完成后是否立即校验(默认 `false`) */
64
+ validateOnInit?: boolean;
58
65
  disabled?: boolean;
59
66
  size?: 'small' | 'default' | 'large';
60
67
  confirmText?: string;
61
68
  inline?: boolean;
62
69
  labelPosition?: string;
63
70
  preventSubmitDefault?: boolean;
71
+ /** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
72
+ useFieldTextInError?: boolean;
64
73
  extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
65
74
  }>(),
66
75
  {
67
76
  config: () => [],
68
77
  values: () => ({}),
69
78
  confirmText: '确定',
79
+ useFieldTextInError: true,
80
+ validateOnInit: false,
70
81
  },
71
82
  );
72
83
 
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <TMagicDialog
3
3
  v-model="dialogVisible"
4
- class="m-form-dialog"
4
+ :class="['m-form-dialog', effectiveTheme ? `m-theme--${effectiveTheme}` : '']"
5
5
  top="20px"
6
6
  append-to-body
7
7
  :title="title"
@@ -31,7 +31,11 @@
31
31
  :label-position="labelPosition"
32
32
  :inline="inline"
33
33
  :prevent-submit-default="preventSubmitDefault"
34
+ :use-field-text-in-error="useFieldTextInError"
35
+ :type-match-valid="typeMatchValid"
36
+ :validate-on-init="validateOnInit"
34
37
  :extend-state="extendState"
38
+ :theme="effectiveTheme"
35
39
  @change="changeHandler"
36
40
  ></Form>
37
41
  <slot></slot>
@@ -64,9 +68,9 @@
64
68
  </template>
65
69
 
66
70
  <script setup lang="ts">
67
- import { computed, ref } from 'vue';
71
+ import { computed, inject, provide, ref } from 'vue';
68
72
 
69
- import { TMagicButton, TMagicCol, TMagicDialog, TMagicRow } from '@tmagic/design';
73
+ import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDialog, TMagicRow } from '@tmagic/design';
70
74
 
71
75
  import Form from './Form.vue';
72
76
  import { ContainerChangeEventData, FormConfig, FormState, FormValue, StepConfig } from './schema';
@@ -82,6 +86,10 @@ const props = withDefaults(
82
86
  parentValues?: Object;
83
87
  width?: string | number;
84
88
  labelWidth?: string;
89
+ /** 是否开启类型匹配校验 */
90
+ typeMatchValid?: boolean;
91
+ /** 透传给内部 `MForm`,初始化完成后是否立即校验(默认 `false`) */
92
+ validateOnInit?: boolean;
85
93
  fullscreen?: boolean;
86
94
  disabled?: boolean;
87
95
  title?: string;
@@ -96,8 +104,16 @@ const props = withDefaults(
96
104
  destroyOnClose?: boolean;
97
105
  showClose?: boolean;
98
106
  showCancel?: boolean;
107
+ /** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
108
+ useFieldTextInError?: boolean;
99
109
  /** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
100
110
  extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
111
+ /**
112
+ * 主题名。优先级:传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串。
113
+ * 计算结果会再次 `provide` 出去,使得 Dialog 被 Teleport 到 body 后,内部子树
114
+ * (包括 `MForm` 以及更深的 popover / dropdown)仍能拿到主题。
115
+ */
116
+ theme?: string;
101
117
  }>(),
102
118
  {
103
119
  config: () => [],
@@ -108,9 +124,15 @@ const props = withDefaults(
108
124
  destroyOnClose: false,
109
125
  showClose: true,
110
126
  showCancel: true,
127
+ useFieldTextInError: true,
128
+ validateOnInit: false,
111
129
  },
112
130
  );
113
131
 
132
+ const ancestorTheme = inject(M_THEME_KEY, null);
133
+ const effectiveTheme = computed(() => props.theme || ancestorTheme?.value || '');
134
+ provide(M_THEME_KEY, effectiveTheme);
135
+
114
136
  const emit = defineEmits(['close', 'submit', 'error', 'change']);
115
137
 
116
138
  const form = ref<InstanceType<typeof Form>>();
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <TMagicDrawer
3
- class="m-form-drawer"
3
+ :class="['m-form-drawer', effectiveTheme ? `m-theme--${effectiveTheme}` : '']"
4
4
  ref="drawer"
5
- v-model="visible"
5
+ v-model="dialogVisible"
6
6
  :title="title"
7
7
  :close-on-press-escape="closeOnPressEscape"
8
8
  :append-to-body="true"
@@ -16,7 +16,7 @@
16
16
  @close="closeHandler"
17
17
  @closed="closedHandler"
18
18
  >
19
- <div v-if="visible" ref="drawerBody" class="m-drawer-body">
19
+ <div v-if="dialogVisible" ref="drawerBody" class="m-drawer-body">
20
20
  <Form
21
21
  ref="form"
22
22
  :size="size"
@@ -28,7 +28,11 @@
28
28
  :label-position="labelPosition"
29
29
  :inline="inline"
30
30
  :prevent-submit-default="preventSubmitDefault"
31
+ :use-field-text-in-error="useFieldTextInError"
32
+ :type-match-valid="typeMatchValid"
33
+ :validate-on-init="validateOnInit"
31
34
  :extend-state="extendState"
35
+ :theme="effectiveTheme"
32
36
  @change="changeHandler"
33
37
  ></Form>
34
38
  <slot></slot>
@@ -55,24 +59,28 @@
55
59
  </template>
56
60
 
57
61
  <script setup lang="ts">
58
- import { ref, watchEffect } from 'vue';
62
+ import { computed, inject, provide, ref, watchEffect } from 'vue';
59
63
 
60
- import { TMagicButton, TMagicCol, TMagicDrawer, TMagicRow } from '@tmagic/design';
64
+ import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDrawer, TMagicRow } from '@tmagic/design';
61
65
 
62
66
  import Form from './Form.vue';
63
67
  import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from './schema';
64
68
 
65
69
  defineOptions({
66
- name: 'MFormDialog',
70
+ name: 'MFormDrawer',
67
71
  });
68
72
 
69
- withDefaults(
73
+ const props = withDefaults(
70
74
  defineProps<{
71
75
  config?: FormConfig;
72
76
  values?: Object;
73
77
  parentValues?: Object;
74
78
  width?: string | number;
75
79
  labelWidth?: string;
80
+ /** 是否开启类型匹配校验 */
81
+ typeMatchValid?: boolean;
82
+ /** 透传给内部 `MForm`,初始化完成后是否立即校验(默认 `false`) */
83
+ validateOnInit?: boolean;
76
84
  disabled?: boolean;
77
85
  closeOnPressEscape?: boolean;
78
86
  title?: string;
@@ -82,25 +90,39 @@ withDefaults(
82
90
  inline?: boolean;
83
91
  labelPosition?: string;
84
92
  preventSubmitDefault?: boolean;
93
+ /** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */
94
+ useFieldTextInError?: boolean;
85
95
  /** 关闭前的回调,会暂停 Drawer 的关闭; done 是个 function type 接受一个 boolean 参数, 执行 done 使用 true 参数或不提供参数将会终止关闭 */
86
96
  beforeClose?: (_done: (_cancel?: boolean) => void) => void;
87
97
  /** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
88
98
  extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
99
+ /**
100
+ * 主题名。优先级:传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串。
101
+ * 计算结果会再次 `provide` 出去,使得 Drawer 被 Teleport 到 body 后,内部子树
102
+ * (包括 `MForm` 以及更深的 popover / dropdown)仍能拿到主题。
103
+ */
104
+ theme?: string;
89
105
  }>(),
90
106
  {
91
107
  closeOnPressEscape: true,
92
108
  config: () => [],
93
109
  values: () => ({}),
94
110
  confirmText: '确定',
111
+ useFieldTextInError: true,
112
+ validateOnInit: false,
95
113
  },
96
114
  );
97
115
 
116
+ const ancestorTheme = inject(M_THEME_KEY, null);
117
+ const effectiveTheme = computed(() => props.theme || ancestorTheme?.value || '');
118
+ provide(M_THEME_KEY, effectiveTheme);
119
+
98
120
  const emit = defineEmits(['close', 'closed', 'submit', 'error', 'change', 'open', 'opened']);
99
121
 
100
122
  const drawer = ref<InstanceType<typeof TMagicDrawer>>();
101
123
  const form = ref<InstanceType<typeof Form>>();
102
124
  const drawerBody = ref<HTMLDivElement>();
103
- const visible = ref(false);
125
+ const dialogVisible = ref(false);
104
126
  const saveFetch = ref(false);
105
127
  const bodyHeight = ref(0);
106
128
 
@@ -141,25 +163,42 @@ const closedHandler = () => {
141
163
  };
142
164
 
143
165
  const show = () => {
144
- visible.value = true;
166
+ dialogVisible.value = true;
145
167
  };
146
168
 
147
169
  const hide = () => {
148
- visible.value = false;
170
+ dialogVisible.value = false;
171
+ };
172
+ const close = () => {
173
+ dialogVisible.value = false;
174
+ };
175
+ const cancel = () => {
176
+ hide();
149
177
  };
150
-
151
178
  /** 用于关闭 Drawer, 该方法会调用传入的 before-close 方法 */
152
179
  const handleClose = () => {
153
180
  drawer.value?.handleClose();
154
181
  };
155
182
 
183
+ const save = async () => {
184
+ try {
185
+ const changeRecords = [...(form.value?.changeRecords || [])];
186
+ const values = await form.value?.submitForm();
187
+ emit('submit', values, { changeRecords });
188
+ } catch (e) {
189
+ emit('error', e);
190
+ }
191
+ };
192
+
156
193
  defineExpose({
157
194
  form,
158
195
  saveFetch,
159
196
  bodyHeight,
160
-
197
+ close,
161
198
  show,
162
199
  hide,
200
+ save,
201
+ cancel,
163
202
  handleClose,
164
203
  });
165
204
  </script>
@@ -12,6 +12,7 @@
12
12
  :disabled="disabled"
13
13
  @change="changeHandler"
14
14
  @add-diff-count="onAddDiffCount"
15
+ :is-in-row="true"
15
16
  ></Container>
16
17
  </TMagicCol>
17
18
  </template>