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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/es/Form.vue_vue_type_script_setup_true_lang.js +47 -120
  2. package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +3 -3
  3. package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +3 -3
  4. package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +3 -3
  5. package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +3 -0
  6. package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +36 -51
  7. package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +6 -0
  8. package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +4 -1
  9. package/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +37 -15
  10. package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +109 -97
  11. package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +7 -2
  12. package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +3 -0
  13. package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +4 -1
  14. package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +7 -0
  15. package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +7 -5
  16. package/dist/es/containers/table/useTableColumns.js +5 -13
  17. package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +71 -78
  18. package/dist/es/containers/table-group-list/useAdd.js +26 -16
  19. package/dist/es/containers/table-group-list/useScrollLastItemIntoView.js +72 -0
  20. package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +1 -1
  21. package/dist/es/fields/CheckboxGroup/Index.js +5 -0
  22. package/dist/es/fields/{CheckboxGroup.vue_vue_type_script_setup_true_lang.js → CheckboxGroup/Index.vue_vue_type_script_setup_true_lang.js} +6 -7
  23. package/dist/es/fields/CheckboxGroup/effect.js +8 -0
  24. package/dist/es/fields/Date/Index.js +5 -0
  25. package/dist/es/fields/{Date.vue_vue_type_script_setup_true_lang.js → Date/Index.vue_vue_type_script_setup_true_lang.js} +5 -7
  26. package/dist/es/fields/Date/effect.js +10 -0
  27. package/dist/es/fields/DateTime/Index.js +5 -0
  28. package/dist/es/fields/{DateTime.vue_vue_type_script_setup_true_lang.js → DateTime/Index.vue_vue_type_script_setup_true_lang.js} +5 -9
  29. package/dist/es/fields/DateTime/effect.js +15 -0
  30. package/dist/es/fields/Display/Index.js +5 -0
  31. package/dist/es/fields/{Display.vue_vue_type_script_setup_true_lang.js → Display/Index.vue_vue_type_script_setup_true_lang.js} +6 -7
  32. package/dist/es/fields/Display/effect.js +8 -0
  33. package/dist/es/fields/DynamicField/Index.js +5 -0
  34. package/dist/es/fields/{DynamicField.vue_vue_type_script_setup_true_lang.js → DynamicField/Index.vue_vue_type_script_setup_true_lang.js} +11 -15
  35. package/dist/es/fields/DynamicField/effect.js +40 -0
  36. package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +1 -1
  37. package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +1 -1
  38. package/dist/es/fields/NumberRange/Index.js +5 -0
  39. package/dist/es/fields/{NumberRange.vue_vue_type_script_setup_true_lang.js → NumberRange/Index.vue_vue_type_script_setup_true_lang.js} +5 -6
  40. package/dist/es/fields/NumberRange/effect.js +8 -0
  41. package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +13 -6
  42. package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +1 -1
  43. package/dist/es/headless.js +13 -0
  44. package/dist/es/index.js +17 -12
  45. package/dist/es/plugin.js +57 -41
  46. package/dist/es/schema.js +6 -8
  47. package/dist/es/style.css +81 -1
  48. package/dist/es/submitForm.js +44 -194
  49. package/dist/es/utils/builtInFields.js +49 -0
  50. package/dist/es/utils/collectFields.js +407 -0
  51. package/dist/es/utils/config.js +1 -8
  52. package/dist/es/utils/fieldInnerConfig.js +42 -0
  53. package/dist/es/utils/fieldValueEffects.js +80 -0
  54. package/dist/es/utils/form.js +96 -43
  55. package/dist/es/utils/formStateProxy.js +111 -0
  56. package/dist/es/utils/registerField.js +158 -0
  57. package/dist/es/utils/submitHeadless.js +54 -0
  58. package/dist/es/utils/tableGroupList.js +85 -0
  59. package/dist/es/utils/typeMatch.js +17 -19
  60. package/dist/es/utils/validateError.js +57 -0
  61. package/dist/es/utils/validateValues.js +115 -0
  62. package/dist/style.css +81 -1
  63. package/dist/themes/magic-admin.css +83 -7
  64. package/dist/tmagic-form-headless.umd.cjs +4080 -0
  65. package/dist/tmagic-form.umd.cjs +2020 -876
  66. package/package.json +10 -4
  67. package/src/Form.vue +49 -149
  68. package/src/FormBox.vue +5 -4
  69. package/src/FormDialog.vue +5 -5
  70. package/src/FormDrawer.vue +5 -5
  71. package/src/containers/Col.vue +2 -0
  72. package/src/containers/Container.vue +86 -157
  73. package/src/containers/Fieldset.vue +5 -0
  74. package/src/containers/FlexLayout.vue +2 -0
  75. package/src/containers/GroupList.vue +41 -11
  76. package/src/containers/GroupListItem.vue +83 -80
  77. package/src/containers/Panel.vue +3 -0
  78. package/src/containers/Row.vue +2 -0
  79. package/src/containers/Step.vue +2 -0
  80. package/src/containers/Tabs.vue +13 -0
  81. package/src/containers/table/ActionsColumn.vue +1 -1
  82. package/src/containers/table/Table.vue +4 -3
  83. package/src/containers/table/usePagination.ts +1 -1
  84. package/src/containers/table/useSortable.ts +1 -1
  85. package/src/containers/table/useTableColumns.ts +9 -19
  86. package/src/containers/table-group-list/TableGroupList.vue +43 -71
  87. package/src/containers/table-group-list/useAdd.ts +40 -25
  88. package/src/containers/table-group-list/useScrollLastItemIntoView.ts +94 -0
  89. package/src/fields/{CheckboxGroup.vue → CheckboxGroup/Index.vue} +3 -8
  90. package/src/fields/CheckboxGroup/effect.ts +27 -0
  91. package/src/fields/{Date.vue → Date/Index.vue} +2 -5
  92. package/src/fields/Date/effect.ts +29 -0
  93. package/src/fields/{DateTime.vue → DateTime/Index.vue} +2 -16
  94. package/src/fields/DateTime/effect.ts +36 -0
  95. package/src/fields/{Display.vue → Display/Index.vue} +3 -7
  96. package/src/fields/Display/effect.ts +28 -0
  97. package/src/fields/{DynamicField.vue → DynamicField/Index.vue} +11 -11
  98. package/src/fields/DynamicField/effect.ts +74 -0
  99. package/src/fields/{NumberRange.vue → NumberRange/Index.vue} +2 -6
  100. package/src/fields/NumberRange/effect.ts +27 -0
  101. package/src/fields/Select.vue +13 -4
  102. package/src/headless.ts +78 -0
  103. package/src/index.ts +32 -15
  104. package/src/plugin.ts +63 -42
  105. package/src/schema.ts +6 -8
  106. package/src/submitForm.ts +81 -409
  107. package/src/theme/group-list.scss +107 -1
  108. package/src/theme/themes/magic-admin/index.scss +2 -8
  109. package/src/utils/builtInFields.ts +69 -0
  110. package/src/utils/collectFields.ts +561 -0
  111. package/src/utils/config.ts +1 -15
  112. package/src/utils/fieldInnerConfig.ts +127 -0
  113. package/src/utils/fieldValueEffects.ts +140 -0
  114. package/src/utils/form.ts +141 -75
  115. package/src/utils/formStateProxy.ts +149 -0
  116. package/src/utils/registerField.ts +308 -0
  117. package/src/utils/submitHeadless.ts +206 -0
  118. package/src/utils/tableGroupList.ts +120 -0
  119. package/src/utils/typeMatch.ts +19 -15
  120. package/src/utils/validateError.ts +92 -0
  121. package/src/utils/validateValues.ts +177 -0
  122. package/types/headless.d.ts +682 -0
  123. package/types/index.d.ts +631 -272
  124. package/dist/es/fields/CheckboxGroup.js +0 -5
  125. package/dist/es/fields/Date.js +0 -5
  126. package/dist/es/fields/DateTime.js +0 -5
  127. package/dist/es/fields/Display.js +0 -5
  128. package/dist/es/fields/DynamicField.js +0 -5
  129. package/dist/es/fields/NumberRange.js +0 -5
  130. package/dist/es/utils/silentLeafFieldTypes.js +0 -49
  131. package/src/utils/silentLeafFieldTypes.ts +0 -71
@@ -3,7 +3,9 @@ import { computed, inject } from 'vue';
3
3
  import { tMagicMessage } from '@tmagic/design';
4
4
  import type { FormConfig, FormState, TableConfig, TableGroupListCommonConfig } from '@tmagic/form-schema';
5
5
 
6
- import { initValue } from '../../utils/form';
6
+ import { applyMountValueEffects } from '@form/utils/collectFields';
7
+ import { initValue } from '@form/utils/form';
8
+
7
9
  import type { TableProps } from '../table/type';
8
10
 
9
11
  export const useAdd = (
@@ -22,54 +24,63 @@ export const useAdd = (
22
24
 
23
25
  if (!modelName) return false;
24
26
 
25
- if (!props.model[modelName]?.length) {
26
- return true;
27
+ if (typeof props.config.addable === 'function') {
28
+ return Boolean(
29
+ props.config.addable(mForm, {
30
+ model: props.model[modelName],
31
+ formValue: mForm?.values,
32
+ prop: props.prop,
33
+ config: props.config,
34
+ }),
35
+ );
27
36
  }
28
37
 
29
- if (typeof props.config.addable === 'function') {
30
- return props.config.addable(mForm, {
31
- model: props.model[modelName],
32
- formValue: mForm?.values,
33
- prop: props.prop,
34
- config: props.config,
35
- });
38
+ if (!props.model[modelName]?.length) {
39
+ return true;
36
40
  }
37
41
 
38
- return typeof props.config.addable === 'undefined' ? true : props.config.addable;
42
+ return typeof props.config.addable === 'undefined' ? true : Boolean(props.config.addable);
39
43
  });
40
44
 
41
- const newHandler = async (row?: any) => {
42
- const modelName = props.name || props.config.name || '';
43
-
44
- if (props.config.max && props.model[modelName].length >= props.config.max) {
45
+ /**
46
+ * 新增一项,返回新增后列表应有的长度;没有新增(超上限 / 被 beforeAddRow 拦下 / enum 用尽)返回 null。
47
+ *
48
+ * 只抛 change,不直接改 `props.model`:写回由 `MForm` 按 changeRecords 的 propPath 完成。
49
+ * 返回长度是给调用方用的——新项要等写回后才出现在 DOM 里,靠它才能判断等到了没有。
50
+ */
51
+ const newHandler = async (row?: any): Promise<number | null> => {
52
+ const modelName = `${props.name || props.config.name || ''}`;
53
+ const list: any[] = Array.isArray(props.model[modelName]) ? props.model[modelName] : [];
54
+
55
+ if (props.config.max && list.length >= props.config.max) {
45
56
  tMagicMessage.error(`最多新增配置不能超过${props.config.max}条`);
46
- return;
57
+ return null;
47
58
  }
48
59
 
49
60
  if (typeof props.config.beforeAddRow === 'function') {
50
61
  const beforeCheckRes = await props.config.beforeAddRow(mForm, {
51
- model: props.model[modelName],
62
+ model: list,
52
63
  formValue: mForm?.values,
53
64
  prop: props.prop,
54
65
  });
55
- if (!beforeCheckRes) return;
66
+ if (!beforeCheckRes) return null;
56
67
  }
57
68
 
58
69
  const columns = props.config.items;
59
70
  const enumValues = props.config.enum || [];
60
71
  let enumV = [];
61
- const { length } = props.model[modelName];
72
+ const { length } = list;
62
73
  const key = props.config.key || 'id';
63
74
  let inputs: any = {};
64
75
 
65
76
  if (enumValues.length) {
66
77
  if (length >= enumValues.length) {
67
- return;
78
+ return null;
68
79
  }
69
80
  enumV = enumValues.filter((item) => {
70
81
  let i = 0;
71
82
  for (; i < length; i++) {
72
- if (item[key] === props.model[modelName][i][key]) {
83
+ if (item[key] === list[i][key]) {
73
84
  break;
74
85
  }
75
86
  }
@@ -87,7 +98,7 @@ export const useAdd = (
87
98
  } else {
88
99
  if (typeof props.config.defaultAdd === 'function') {
89
100
  inputs = await props.config.defaultAdd(mForm, {
90
- model: props.model[modelName],
101
+ model: list,
91
102
  prop: props.prop,
92
103
  formValue: mForm?.values,
93
104
  });
@@ -101,18 +112,22 @@ export const useAdd = (
101
112
  });
102
113
  }
103
114
 
115
+ // enum / Excel 导入的数组行与默认新增共用同一份规整,字段组件不再在 setup 里改 model
116
+ applyMountValueEffects(mForm, columns as FormConfig, inputs);
117
+
104
118
  if (props.sortKey && length) {
105
- inputs[props.sortKey] = props.model[modelName][length - 1][props.sortKey] - 1;
119
+ inputs[props.sortKey] = list[length - 1][props.sortKey] - 1;
106
120
  }
107
121
 
108
- emit('change', [...props.model[modelName], inputs], {
122
+ emit('change', [...list, inputs], {
109
123
  changeRecords: [
110
124
  {
111
- propPath: `${props.prop}.${props.model[modelName].length}`,
125
+ propPath: `${props.prop}.${length}`,
112
126
  value: inputs,
113
127
  },
114
128
  ],
115
129
  });
130
+ return length + 1;
116
131
  };
117
132
 
118
133
  return {
@@ -0,0 +1,94 @@
1
+ import { nextTick, type Ref } from 'vue';
2
+
3
+ type ListRef = Ref<{ $el?: unknown } | HTMLElement | null | undefined>;
4
+
5
+ interface ScrollTarget {
6
+ root: HTMLElement;
7
+ last: HTMLElement | null;
8
+ count: number;
9
+ }
10
+
11
+ const nextFrame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
12
+
13
+ /** 滚动要用的列表根与最后一项;组件已卸载(宿主重建了整棵表单)时返回 null */
14
+ const resolveScrollTarget = (listRef: ListRef): ScrollTarget | null => {
15
+ const inst = listRef.value;
16
+ const root = inst instanceof HTMLElement ? inst : inst?.$el;
17
+ if (!(root instanceof HTMLElement)) return null;
18
+
19
+ const items = root.querySelectorAll(':scope > .m-fields-group-list-item');
20
+ const last = items[items.length - 1];
21
+ return { root, last: last instanceof HTMLElement ? last : null, count: items.length };
22
+ };
23
+
24
+ /** 吸顶标题自身的高度:滚动后新项的标题不能被上一层标题压住 */
25
+ const getStickyHeaderOffset = (item: HTMLElement): number => {
26
+ const header = item.querySelector(':scope > .el-card__header, :scope > .t-card__header');
27
+ if (!(header instanceof HTMLElement)) return 0;
28
+ return Number.parseFloat(getComputedStyle(header).top) || 0;
29
+ };
30
+
31
+ /** 吸底「新增」按钮盖住的高度;嵌套列表的 footer 还用 bottom 给外层按钮留了位 */
32
+ const getStickyFooterOffset = (root: HTMLElement): number => {
33
+ const footer = root.querySelector(':scope > .m-fields-group-list-footer.is-sticky-full');
34
+ if (!(footer instanceof HTMLElement)) return 0;
35
+ return footer.getBoundingClientRect().height + (Number.parseFloat(getComputedStyle(footer).bottom) || 0);
36
+ };
37
+
38
+ /**
39
+ * 等列表渲染出至少 `expectedCount` 项、且最后一项的 DOM 节点连续两帧不变再返回。
40
+ *
41
+ * 新增只抛 change,写回要经过宿主(如编辑器属性面板)的异步校验与表单值重建,
42
+ * 新项不一定当帧就出现;期间宿主还可能自己再追加项,末尾节点会换掉。
43
+ * 只判「节点不变」会滚到旧的最后一项上,所以先等够数量再等稳定。
44
+ */
45
+ const waitForStableTarget = async (
46
+ listRef: ListRef,
47
+ expectedCount: number,
48
+ timeout = 600,
49
+ ): Promise<ScrollTarget | null> => {
50
+ const deadline = Date.now() + timeout;
51
+ let previous: HTMLElement | null = null;
52
+
53
+ while (Date.now() < deadline) {
54
+ const target = resolveScrollTarget(listRef);
55
+ if (!target) return null;
56
+ if (target.count >= expectedCount && target.last && target.last === previous) return target;
57
+ previous = target.last;
58
+ await nextFrame();
59
+ }
60
+
61
+ return resolveScrollTarget(listRef);
62
+ };
63
+
64
+ /**
65
+ * 新增后把最后一项滚进视口,避开吸顶标题与吸底按钮。
66
+ *
67
+ * `enabled` 为 false(未开配置、或不在 group-list 形态)时直接返回。
68
+ */
69
+ export const useScrollLastItemIntoView = (listRef: ListRef, enabled: () => boolean) => {
70
+ const scrollLastItemIntoView = async (expectedCount: number) => {
71
+ if (!enabled()) return;
72
+
73
+ // 先让已经同步写回的那部分渲染出来,稳定判定的第一帧就能拿到新项,少等一帧
74
+ await nextTick();
75
+
76
+ const target = await waitForStableTarget(listRef, expectedCount);
77
+ if (!target?.last) return;
78
+
79
+ const { root, last } = target;
80
+
81
+ // 新项是追加在末尾的,下方往往没有多余内容可滚,用 `start` 会被浏览器夹住、
82
+ // 仍压在吸底按钮下面。`nearest` 只滚到刚好露出,配合两侧 scroll-margin 避开吸顶标题与吸底按钮。
83
+ const top = getStickyHeaderOffset(last);
84
+ const bottom = getStickyFooterOffset(root);
85
+ last.style.scrollMarginTop = top ? `${top}px` : '';
86
+ last.style.scrollMarginBottom = bottom ? `${bottom}px` : '';
87
+
88
+ // 用瞬时定位而不是 smooth:平滑滚动要持续几百毫秒,期间宿主的回写重绘会把滚动位置改掉,
89
+ // 动画继续奔向旧目标,看起来就是「先弹回顶部再滚下来」。瞬时定位没有这个窗口。
90
+ last.scrollIntoView({ behavior: 'auto', block: 'nearest' });
91
+ };
92
+
93
+ return { scrollLastItemIntoView };
94
+ };
@@ -11,9 +11,9 @@ import { computed, inject } from 'vue';
11
11
 
12
12
  import { TMagicCheckbox, TMagicCheckboxGroup } from '@tmagic/design';
13
13
 
14
- import type { CheckboxGroupConfig, CheckboxGroupOption, FieldProps, FormState } from '../schema';
15
- import { filterFunction } from '../utils/form';
16
- import { useAddField } from '../utils/useAddField';
14
+ import type { CheckboxGroupConfig, CheckboxGroupOption, FieldProps, FormState } from '@form/schema';
15
+ import { filterFunction } from '@form/utils/form';
16
+ import { useAddField } from '@form/utils/useAddField';
17
17
 
18
18
  defineOptions({
19
19
  name: 'MFormCheckGroup',
@@ -25,11 +25,6 @@ const emit = defineEmits(['change']);
25
25
 
26
26
  useAddField(props.prop);
27
27
 
28
- // 初始化选项
29
- if (props.model && !props.model[props.name]) {
30
- props.model[props.name] = [];
31
- }
32
-
33
28
  const changeHandler = (v: Array<string | number | boolean>) => {
34
29
  emit('change', v);
35
30
  };
@@ -0,0 +1,27 @@
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 { FieldMountValueEffect } from '@form/utils/fieldValueEffects';
20
+
21
+ /** 空值初始化为空数组。 */
22
+ export const effect: FieldMountValueEffect = ({ config, model }) => {
23
+ const { name } = config as any;
24
+ if (model && !model[name]) {
25
+ model[name] = [];
26
+ }
27
+ };
@@ -14,9 +14,8 @@
14
14
  <script lang="ts" setup>
15
15
  import { TMagicDatePicker } from '@tmagic/design';
16
16
 
17
- import type { DateConfig, FieldProps } from '../schema';
18
- import { datetimeFormatter } from '../utils/form';
19
- import { useAddField } from '../utils/useAddField';
17
+ import type { DateConfig, FieldProps } from '@form/schema';
18
+ import { useAddField } from '@form/utils/useAddField';
20
19
 
21
20
  defineOptions({
22
21
  name: 'MFormDate',
@@ -30,8 +29,6 @@ const emit = defineEmits<{
30
29
 
31
30
  useAddField(props.prop);
32
31
 
33
- props.model[props.name] = datetimeFormatter(props.model[props.name], '', props.config.valueFormat || 'YYYY/MM/DD');
34
-
35
32
  const changeHandler = (v: string) => {
36
33
  emit('change', v);
37
34
  };
@@ -0,0 +1,29 @@
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 { DateConfig } from '@form/schema';
20
+ import type { FieldMountValueEffect } from '@form/utils/fieldValueEffects';
21
+ import { datetimeFormatter } from '@form/utils/form';
22
+
23
+ /** 按 `valueFormat` 归一化日期值。 */
24
+ export const effect: FieldMountValueEffect = ({ config, model }) => {
25
+ if (!model) return;
26
+
27
+ const { name, valueFormat } = config as DateConfig & { name: string };
28
+ model[name] = datetimeFormatter(model[name], '', valueFormat || 'YYYY/MM/DD');
29
+ };
@@ -16,9 +16,8 @@
16
16
  <script lang="ts" setup>
17
17
  import { TMagicDatePicker } from '@tmagic/design';
18
18
 
19
- import type { DateTimeConfig, FieldProps } from '../schema';
20
- import { datetimeFormatter } from '../utils/form';
21
- import { useAddField } from '../utils/useAddField';
19
+ import type { DateTimeConfig, FieldProps } from '@form/schema';
20
+ import { useAddField } from '@form/utils/useAddField';
22
21
 
23
22
  defineOptions({
24
23
  name: 'MFormDateTime',
@@ -32,19 +31,6 @@ const emit = defineEmits<{
32
31
 
33
32
  useAddField(props.prop);
34
33
 
35
- const value = props.model?.[props.name]?.toString();
36
- if (props.model) {
37
- if (!value || value === 'Invalid Date') {
38
- props.model[props.name] = '';
39
- } else {
40
- props.model[props.name] = datetimeFormatter(
41
- props.model[props.name],
42
- '',
43
- props.config.valueFormat || 'YYYY/MM/DD HH:mm:ss',
44
- );
45
- }
46
- }
47
-
48
34
  const changeHandler = (v: string) => {
49
35
  emit('change', v);
50
36
  };
@@ -0,0 +1,36 @@
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 { DateTimeConfig } from '@form/schema';
20
+ import type { FieldMountValueEffect } from '@form/utils/fieldValueEffects';
21
+ import { datetimeFormatter } from '@form/utils/form';
22
+
23
+ /** 按 `valueFormat` 归一化日期时间值,空值与非法值统一为空字符串。 */
24
+ export const effect: FieldMountValueEffect = ({ config, model }) => {
25
+ if (!model) return;
26
+
27
+ const { name, valueFormat } = config as DateTimeConfig & { name: string };
28
+ const value = model[name]?.toString();
29
+
30
+ if (!value || value === 'Invalid Date') {
31
+ model[name] = '';
32
+ return;
33
+ }
34
+
35
+ model[name] = datetimeFormatter(model[name], '', valueFormat || 'YYYY/MM/DD HH:mm:ss');
36
+ };
@@ -5,9 +5,9 @@
5
5
  <script setup lang="ts">
6
6
  import { computed, inject } from 'vue';
7
7
 
8
- import type { DisplayConfig, FieldProps, FormState } from '../schema';
9
- import { filterFunction } from '../utils/form';
10
- import { useAddField } from '../utils/useAddField';
8
+ import type { DisplayConfig, FieldProps, FormState } from '@form/schema';
9
+ import { filterFunction } from '@form/utils/form';
10
+ import { useAddField } from '@form/utils/useAddField';
11
11
 
12
12
  defineOptions({
13
13
  name: 'MFormDisplay',
@@ -17,10 +17,6 @@ const props = defineProps<FieldProps<DisplayConfig>>();
17
17
 
18
18
  const mForm = inject<FormState | undefined>('mForm');
19
19
 
20
- if (props.config.initValue && props.model) {
21
- props.model[props.name] = props.config.initValue;
22
- }
23
-
24
20
  const text = computed(() => {
25
21
  if (props.config.displayText) {
26
22
  return filterFunction<string>(mForm, props.config.displayText, props);
@@ -0,0 +1,28 @@
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 { DisplayConfig } from '@form/schema';
20
+ import type { FieldMountValueEffect } from '@form/utils/fieldValueEffects';
21
+
22
+ /** 把 `initValue` 写入 model。 */
23
+ export const effect: FieldMountValueEffect = ({ config, model }) => {
24
+ const { initValue, name } = config as DisplayConfig & { name: string };
25
+ if (initValue && model) {
26
+ model[name] = initValue;
27
+ }
28
+ };
@@ -27,9 +27,11 @@ import { onBeforeUnmount, reactive, watch } from 'vue';
27
27
 
28
28
  import { TMagicForm, TMagicFormItem, TMagicInput } from '@tmagic/design';
29
29
 
30
- import type { DynamicFieldConfig, FieldProps } from '../schema';
31
- import { getConfig } from '../utils/config';
32
- import { useAddField } from '../utils/useAddField';
30
+ import type { DynamicFieldConfig, FieldProps } from '@form/schema';
31
+ import { getConfig } from '@form/utils/config';
32
+ import { useAddField } from '@form/utils/useAddField';
33
+
34
+ import { eachDynamicField } from './effect';
33
35
 
34
36
  defineOptions({
35
37
  name: 'MFormDynamicField',
@@ -54,15 +56,13 @@ const changeFieldMap = async () => {
54
56
  const fields = await props.config.returnFields(props.config, props.model, request);
55
57
  fieldMap.value = {};
56
58
  fieldLabelMap.value = {};
57
- fields.forEach((v) => {
58
- if (typeof v !== 'object' || v.name === undefined) return;
59
- let oldVal = props.model?.[v.name] || '';
60
- if (!oldVal && v.defaultValue !== undefined) {
61
- oldVal = v.defaultValue;
62
- emit('change', oldVal, { modifyKey: v.name });
59
+ eachDynamicField(fields, props.model, (field, value, isDefaultApplied) => {
60
+ // 取了 defaultValue 的字段需要写回表单值
61
+ if (isDefaultApplied) {
62
+ emit('change', value, { modifyKey: field.name });
63
63
  }
64
- fieldMap.value[v.name] = oldVal;
65
- fieldLabelMap.value[v.name] = v.label || '';
64
+ fieldMap.value[field.name] = value;
65
+ fieldLabelMap.value[field.name] = field.label || '';
66
66
  });
67
67
  };
68
68
 
@@ -0,0 +1,74 @@
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 { setValueByKeyPath } from '@tmagic/utils';
20
+
21
+ import type { DynamicFieldConfig, FormValue } from '@form/schema';
22
+ import { getConfig } from '@form/utils/config';
23
+ import type { FieldMountValueEffect } from '@form/utils/fieldValueEffects';
24
+
25
+ /** `dynamic-field` 的 `returnFields` 返回的单个字段描述 */
26
+ type DynamicFieldItem = ReturnType<DynamicFieldConfig['returnFields']>[number];
27
+
28
+ /**
29
+ * 遍历动态字段列表,按「原值为空且声明了 defaultValue 则取默认值」求出每个字段当前的值。
30
+ *
31
+ * `isDefaultApplied` 为真表示该值来自 `defaultValue`,需要写回表单值
32
+ * (组件通过 emit change 写回,初始化写入直接写 `values`)。
33
+ */
34
+ export const eachDynamicField = (
35
+ fields: DynamicFieldItem[],
36
+ model: FormValue | undefined,
37
+ onField: (_field: DynamicFieldItem, _value: any, _isDefaultApplied: boolean) => void,
38
+ ): void => {
39
+ for (const field of fields) {
40
+ if (typeof field !== 'object' || field?.name === undefined) continue;
41
+
42
+ let value = model?.[field.name] || '';
43
+ let isDefaultApplied = false;
44
+
45
+ if (!value && field.defaultValue !== undefined) {
46
+ value = field.defaultValue;
47
+ isDefaultApplied = true;
48
+ }
49
+
50
+ onField(field, value, isDefaultApplied);
51
+ }
52
+ };
53
+
54
+ /**
55
+ * 同步 `returnFields` 的 defaultValue 按 prop 写入值根对象。
56
+ *
57
+ * 该组件读取的是同级 model,但写入走 Container 的 modifyKey 分支,落在 `${prop}.${key}`,
58
+ * 这里保持与渲染一致(含这层不对称),避免两条链路产出不同的值。
59
+ * 异步 returnFields 由组件挂载后的 watch 负责,此处不等待。
60
+ */
61
+ export const effect: FieldMountValueEffect = ({ config, model, prop, values }) => {
62
+ const { returnFields, dynamicKey } = config as DynamicFieldConfig;
63
+ if (typeof returnFields !== 'function' || !model) return;
64
+ if (model[dynamicKey] === '') return;
65
+
66
+ const result = returnFields(config as DynamicFieldConfig, model, getConfig<Function>('request'));
67
+ if (!result || typeof (result as any).then === 'function' || !Array.isArray(result)) return;
68
+
69
+ eachDynamicField(result, model, (field, value, isDefaultApplied) => {
70
+ if (isDefaultApplied) {
71
+ setValueByKeyPath(`${prop}.${field.name}`, value, values || model);
72
+ }
73
+ });
74
+ };
@@ -23,8 +23,8 @@ import { ref, watch } from 'vue';
23
23
 
24
24
  import { TMagicInput } from '@tmagic/design';
25
25
 
26
- import type { FieldProps, NumberRangeConfig } from '../schema';
27
- import { useAddField } from '../utils/useAddField';
26
+ import type { FieldProps, NumberRangeConfig } from '@form/schema';
27
+ import { useAddField } from '@form/utils/useAddField';
28
28
 
29
29
  defineOptions({
30
30
  name: 'MFormNumberRange',
@@ -53,10 +53,6 @@ watch(
53
53
 
54
54
  useAddField(props.prop);
55
55
 
56
- if (!Array.isArray(props.model[props.name])) {
57
- props.model[props.name] = [];
58
- }
59
-
60
56
  const minChangeHandler = (v: string) => {
61
57
  emit('change', [Number(v), props.model[props.name][1]]);
62
58
  };
@@ -0,0 +1,27 @@
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 { FieldMountValueEffect } from '@form/utils/fieldValueEffects';
20
+
21
+ /** 值不是数组时修正为空数组。 */
22
+ export const effect: FieldMountValueEffect = ({ config, model }) => {
23
+ const { name } = config as any;
24
+ if (model && !Array.isArray(model[name])) {
25
+ model[name] = [];
26
+ }
27
+ };
@@ -57,7 +57,7 @@
57
57
  <component
58
58
  v-for="option in options as SelectOption[]"
59
59
  class="tmagic-design-option"
60
- :key="config.valueKey ? option.value[config.valueKey] : option.value"
60
+ :key="config.valueKey ? option.value?.[config.valueKey] : option.value"
61
61
  :is="optionComponent?.component || 'el-option'"
62
62
  v-bind="
63
63
  optionComponent?.props({
@@ -372,7 +372,12 @@ const getInitOption = async () => {
372
372
  });
373
373
  }
374
374
 
375
- let initData = getValueByKeyPath(initRoot || root, res);
375
+ let initData: any;
376
+ try {
377
+ initData = getValueByKeyPath(initRoot || root, res);
378
+ } catch {
379
+ // 响应中不存在 initRoot/root 路径时按无数据处理
380
+ }
376
381
  if (initData) {
377
382
  if (!Array.isArray(initData)) {
378
383
  initData = [initData];
@@ -417,8 +422,12 @@ if (typeof props.config.options === 'function') {
417
422
  const v = props.model[props.name];
418
423
  if (Array.isArray(v) ? !v.length : typeof v === 'undefined') return;
419
424
  if (typeof v === 'undefined' || hasOption(v)) return;
420
- const data = await getInitOption();
421
- setOptions(data);
425
+ try {
426
+ const data = await getInitOption();
427
+ setOptions(data);
428
+ } catch {
429
+ setOptions([]);
430
+ }
422
431
  };
423
432
 
424
433
  watch(