@tmagic/form 1.8.0-manmanyu.9 → 1.8.0

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 (149) hide show
  1. package/dist/es/Form.vue_vue_type_script_setup_true_lang.js +88 -107
  2. package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +21 -5
  3. package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +21 -5
  4. package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +46 -12
  5. package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +6 -2
  6. package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +44 -47
  7. package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +7 -1
  8. package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +5 -2
  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 +59 -22
  11. package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +118 -100
  12. package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +8 -3
  13. package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +4 -1
  14. package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +5 -2
  15. package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +8 -1
  16. package/dist/es/containers/table/ActionsColumn.vue_vue_type_script_setup_true_lang.js +1 -1
  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 +14 -10
  19. package/dist/es/containers/table/useTableColumns.js +10 -15
  20. package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +72 -79
  21. package/dist/es/containers/table-group-list/useAdd.js +26 -16
  22. package/dist/es/containers/table-group-list/useScrollLastItemIntoView.js +72 -0
  23. package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +2 -2
  24. package/dist/es/fields/Checkbox.vue_vue_type_script_setup_true_lang.js +1 -1
  25. package/dist/es/fields/CheckboxGroup/Index.js +5 -0
  26. 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
  27. package/dist/es/fields/CheckboxGroup/effect.js +8 -0
  28. package/dist/es/fields/ColorPicker.vue_vue_type_script_setup_true_lang.js +1 -1
  29. package/dist/es/fields/Date/Index.js +5 -0
  30. 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
  31. package/dist/es/fields/Date/effect.js +10 -0
  32. package/dist/es/fields/DateTime/Index.js +5 -0
  33. 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
  34. package/dist/es/fields/DateTime/effect.js +15 -0
  35. package/dist/es/fields/Daterange.vue_vue_type_script_setup_true_lang.js +2 -2
  36. package/dist/es/fields/Display/Index.js +5 -0
  37. 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
  38. package/dist/es/fields/Display/effect.js +8 -0
  39. package/dist/es/fields/DynamicField/Index.js +5 -0
  40. 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
  41. package/dist/es/fields/DynamicField/effect.js +40 -0
  42. package/dist/es/fields/Hidden.vue_vue_type_script_setup_true_lang.js +1 -1
  43. package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +4 -4
  44. package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +4 -2
  45. package/dist/es/fields/NumberRange/Index.js +5 -0
  46. 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
  47. package/dist/es/fields/NumberRange/effect.js +8 -0
  48. package/dist/es/fields/RadioGroup.vue_vue_type_script_setup_true_lang.js +5 -2
  49. package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +14 -6
  50. package/dist/es/fields/Switch.vue_vue_type_script_setup_true_lang.js +1 -1
  51. package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +13 -10
  52. package/dist/es/fields/Textarea.vue_vue_type_script_setup_true_lang.js +1 -1
  53. package/dist/es/fields/Time.vue_vue_type_script_setup_true_lang.js +1 -1
  54. package/dist/es/fields/Timerange.vue_vue_type_script_setup_true_lang.js +3 -3
  55. package/dist/es/headless.js +13 -0
  56. package/dist/es/index.js +19 -12
  57. package/dist/es/plugin.js +57 -39
  58. package/dist/es/schema.js +11 -2
  59. package/dist/es/style.css +123 -3
  60. package/dist/es/submitForm.js +270 -72
  61. package/dist/es/utils/builtInFields.js +49 -0
  62. package/dist/es/utils/collectFields.js +407 -0
  63. package/dist/es/utils/config.js +4 -8
  64. package/dist/es/utils/fieldInnerConfig.js +42 -0
  65. package/dist/es/utils/fieldValueEffects.js +80 -0
  66. package/dist/es/utils/form.js +154 -19
  67. package/dist/es/utils/formStateProxy.js +111 -0
  68. package/dist/es/utils/registerField.js +158 -0
  69. package/dist/es/utils/submitHeadless.js +54 -0
  70. package/dist/es/utils/tableGroupList.js +104 -0
  71. package/dist/es/utils/typeMatch.js +532 -0
  72. package/dist/es/utils/validateError.js +57 -0
  73. package/dist/es/utils/validateValues.js +115 -0
  74. package/dist/style.css +123 -3
  75. package/dist/themes/magic-admin.css +576 -14
  76. package/dist/tmagic-form-headless.umd.cjs +4515 -0
  77. package/dist/tmagic-form.umd.cjs +3444 -983
  78. package/package.json +16 -10
  79. package/src/Form.vue +113 -129
  80. package/src/FormBox.vue +16 -4
  81. package/src/FormDialog.vue +16 -5
  82. package/src/FormDrawer.vue +41 -13
  83. package/src/containers/Col.vue +3 -0
  84. package/src/containers/Container.vue +40 -45
  85. package/src/containers/Fieldset.vue +5 -0
  86. package/src/containers/FlexLayout.vue +2 -0
  87. package/src/containers/GroupList.vue +64 -13
  88. package/src/containers/GroupListItem.vue +88 -81
  89. package/src/containers/Panel.vue +3 -0
  90. package/src/containers/Row.vue +2 -0
  91. package/src/containers/Step.vue +2 -0
  92. package/src/containers/Tabs.vue +13 -0
  93. package/src/containers/table/ActionsColumn.vue +1 -1
  94. package/src/containers/table/Table.vue +5 -3
  95. package/src/containers/table/usePagination.ts +1 -1
  96. package/src/containers/table/useSortable.ts +1 -1
  97. package/src/containers/table/useTableColumns.ts +15 -22
  98. package/src/containers/table-group-list/TableGroupList.vue +46 -72
  99. package/src/containers/table-group-list/useAdd.ts +40 -25
  100. package/src/containers/table-group-list/useScrollLastItemIntoView.ts +94 -0
  101. package/src/fields/{CheckboxGroup.vue → CheckboxGroup/Index.vue} +3 -8
  102. package/src/fields/CheckboxGroup/effect.ts +27 -0
  103. package/src/fields/{Date.vue → Date/Index.vue} +2 -5
  104. package/src/fields/Date/effect.ts +29 -0
  105. package/src/fields/{DateTime.vue → DateTime/Index.vue} +2 -16
  106. package/src/fields/DateTime/effect.ts +36 -0
  107. package/src/fields/{Display.vue → Display/Index.vue} +3 -7
  108. package/src/fields/Display/effect.ts +28 -0
  109. package/src/fields/{DynamicField.vue → DynamicField/Index.vue} +11 -11
  110. package/src/fields/DynamicField/effect.ts +74 -0
  111. package/src/fields/Link.vue +3 -4
  112. package/src/fields/Number.vue +2 -1
  113. package/src/fields/{NumberRange.vue → NumberRange/Index.vue} +2 -6
  114. package/src/fields/NumberRange/effect.ts +27 -0
  115. package/src/fields/RadioGroup.vue +13 -1
  116. package/src/fields/Select.vue +13 -4
  117. package/src/headless.ts +78 -0
  118. package/src/index.ts +46 -9
  119. package/src/plugin.ts +68 -37
  120. package/src/schema.ts +13 -2
  121. package/src/submitForm.ts +410 -131
  122. package/src/theme/fieldset.scss +1 -0
  123. package/src/theme/form-box.scss +5 -0
  124. package/src/theme/form-dialog.scss +1 -1
  125. package/src/theme/group-list.scss +143 -2
  126. package/src/theme/table.scss +11 -1
  127. package/src/theme/themes/magic-admin/index.scss +27 -16
  128. package/src/utils/builtInFields.ts +69 -0
  129. package/src/utils/collectFields.ts +561 -0
  130. package/src/utils/config.ts +7 -15
  131. package/src/utils/fieldInnerConfig.ts +127 -0
  132. package/src/utils/fieldValueEffects.ts +140 -0
  133. package/src/utils/form.ts +230 -30
  134. package/src/utils/formStateProxy.ts +149 -0
  135. package/src/utils/registerField.ts +308 -0
  136. package/src/utils/submitHeadless.ts +206 -0
  137. package/src/utils/tableGroupList.ts +147 -0
  138. package/src/utils/typeMatch.ts +948 -0
  139. package/src/utils/validateError.ts +92 -0
  140. package/src/utils/validateValues.ts +177 -0
  141. package/types/headless.d.ts +682 -0
  142. package/types/index.d.ts +900 -180
  143. package/dist/es/_virtual/_rolldown/runtime.js +0 -27
  144. package/dist/es/fields/CheckboxGroup.js +0 -5
  145. package/dist/es/fields/Date.js +0 -5
  146. package/dist/es/fields/DateTime.js +0 -5
  147. package/dist/es/fields/Display.js +0 -5
  148. package/dist/es/fields/DynamicField.js +0 -5
  149. package/dist/es/fields/NumberRange.js +0 -5
@@ -70,6 +70,7 @@
70
70
  "
71
71
  :size="size"
72
72
  :label-width="tab.labelWidth || labelWidth"
73
+ :label-position="tab.labelPosition || labelPosition"
73
74
  :expand-more="expandMore"
74
75
  @change="changeHandler"
75
76
  @addDiffCount="onAddDiffCount(Number(tabIndex))"
@@ -85,6 +86,7 @@ import { isEmpty } from 'lodash-es';
85
86
  import { getDesignConfig, TMagicBadge } from '@tmagic/design';
86
87
 
87
88
  import type { ContainerChangeEventData, FormState, TabConfig, TabPaneConfig } from '../schema';
89
+ import { applyMountValueEffects } from '../utils/collectFields';
88
90
  import { display as displayFunc, filterFunction, initValue } from '../utils/form';
89
91
 
90
92
  import Container from './Container.vue';
@@ -106,6 +108,7 @@ const props = withDefaults(
106
108
  name: string;
107
109
  size?: string;
108
110
  labelWidth?: string;
111
+ labelPosition?: 'top' | 'left' | 'right';
109
112
  prop?: string;
110
113
  expandMore?: boolean;
111
114
  disabled?: boolean;
@@ -203,6 +206,13 @@ const onTabAdd = async () => {
203
206
  prop: props.prop,
204
207
  config: props.config,
205
208
  });
209
+ // 自定义回调可能直接 push 原始对象;对现有标签页再规整一遍(effect 必须幂等)
210
+ const tabsModel = props.model[props.name];
211
+ if (Array.isArray(tabsModel) && Array.isArray(props.config.items)) {
212
+ for (const tab of tabsModel) {
213
+ applyMountValueEffects(mForm, props.config.items, tab);
214
+ }
215
+ }
206
216
  emit('change', props.model[props.name]);
207
217
  } else {
208
218
  const newObj = await initValue(mForm, {
@@ -210,6 +220,9 @@ const onTabAdd = async () => {
210
220
  initValues: {},
211
221
  });
212
222
 
223
+ // 新增标签页的值在挂到表单上之前先规整一遍,与整表初始化走同一份登记表
224
+ applyMountValueEffects(mForm, props.config.items, newObj);
225
+
213
226
  newObj.title = `标签${tabs.value.length + 1}`;
214
227
 
215
228
  props.model[props.name].push(newObj);
@@ -43,7 +43,7 @@ import { cloneDeep } from 'lodash-es';
43
43
 
44
44
  import { TMagicButton, TMagicTooltip } from '@tmagic/design';
45
45
 
46
- import type { FormState, TableConfig } from '../../schema';
46
+ import type { FormState, TableConfig } from '@form/schema';
47
47
 
48
48
  const emit = defineEmits(['change']);
49
49
 
@@ -8,13 +8,14 @@
8
8
  >
9
9
  <div class="m-fields-table" :class="{ 'm-fields-table-item-extra': config.itemExtra }">
10
10
  <span v-if="config.extra" style="color: rgba(0, 0, 0, 0.45)" v-html="config.extra"></span>
11
+ <!-- v-if 必须挂在 Tooltip 上:表格未就绪时 ElOnlyChild 会报 no valid child node found -->
11
12
  <TMagicTooltip
13
+ v-if="model[modelName]"
12
14
  content="拖拽可排序"
13
15
  placement="left-start"
14
16
  :disabled="config.dropSort !== true || config.dropSortHandle"
15
17
  >
16
18
  <TMagicTable
17
- v-if="model[modelName]"
18
19
  ref="tMagicTable"
19
20
  style="width: 100%"
20
21
  show-header
@@ -27,6 +28,7 @@
27
28
  :key="updateKey"
28
29
  @select="selectHandle"
29
30
  @sort-change="sortChangeHandler"
31
+ :empty-text="`暂无${config.titlePrefix || ''}数据`"
30
32
  ></TMagicTable>
31
33
  </TMagicTooltip>
32
34
 
@@ -91,8 +93,8 @@ import { FullScreen } from '@element-plus/icons-vue';
91
93
 
92
94
  import { TMagicButton, TMagicPagination, TMagicTable, TMagicTooltip, TMagicUpload } from '@tmagic/design';
93
95
 
94
- import type { SortProp } from '../../schema';
95
- import { sortChange } from '../../utils/form';
96
+ import type { SortProp } from '@form/schema';
97
+ import { sortChange } from '@form/utils/form';
96
98
 
97
99
  import type { TableProps } from './type';
98
100
  import { useFullscreen } from './useFullscreen';
@@ -1,6 +1,6 @@
1
1
  import { computed, type Ref, ref } from 'vue';
2
2
 
3
- import { getDataByPage } from '../../utils/form';
3
+ import { getDataByPage } from '@form/utils/form';
4
4
 
5
5
  import type { TableProps } from './type';
6
6
 
@@ -4,7 +4,7 @@ import type { default as SortableType, SortableEvent } from 'sortablejs';
4
4
  import { type TMagicTable } from '@tmagic/design';
5
5
  import type { FormState } from '@tmagic/form-schema';
6
6
 
7
- import { sortArray } from '../../utils/form';
7
+ import { sortArray } from '@form/utils/form';
8
8
 
9
9
  import type { TableProps } from './type';
10
10
 
@@ -1,12 +1,14 @@
1
1
  import { computed, h, inject, type Ref } from 'vue';
2
2
  import { WarningFilled } from '@element-plus/icons-vue';
3
- import { cloneDeep } from 'lodash-es';
4
3
 
5
4
  import { type TableColumnOptions, TMagicIcon, TMagicTooltip } from '@tmagic/design';
6
- import type { FormItemConfig, FormState, TableColumnConfig } from '@tmagic/form-schema';
5
+ import type { FormItemConfig, FormState } from '@tmagic/form-schema';
6
+
7
+ import type { ContainerChangeEventData } from '@form/schema';
8
+ import { isGlobalFlat } from '@form/utils/config';
9
+ import { appendProp, display as displayFunc, getDataByPage, sortArray } from '@form/utils/form';
10
+ import { isTableColumnRendered, makeTableColumnConfig } from '@form/utils/tableGroupList';
7
11
 
8
- import type { ContainerChangeEventData } from '../../schema';
9
- import { display as displayFunc, getDataByPage, sortArray } from '../../utils/form';
10
12
  import Container from '../Container.vue';
11
13
 
12
14
  import ActionsColumn from './ActionsColumn.vue';
@@ -64,18 +66,7 @@ export const useTableColumns = (
64
66
  return props.config.selection;
65
67
  });
66
68
 
67
- const getProp = (index: number) => {
68
- return `${props.prop}${props.prop ? '.' : ''}${index + 1 + currentPage.value * pageSize.value - 1}`;
69
- };
70
-
71
- const makeConfig = (config: TableColumnConfig, row: any): TableColumnConfig => {
72
- const newConfig = cloneDeep(config);
73
- if (typeof config.itemsFunction === 'function') {
74
- newConfig.items = config.itemsFunction(row);
75
- }
76
- delete newConfig.display;
77
- return newConfig;
78
- };
69
+ const getProp = (index: number) => appendProp(props.prop, index + currentPage.value * pageSize.value);
79
70
 
80
71
  const changeHandler = (v: any, eventData: ContainerChangeEventData) => {
81
72
  emit('change', props.model, eventData);
@@ -111,24 +102,26 @@ export const useTableColumns = (
111
102
  },
112
103
  });
113
104
  }
114
-
115
- let actionFixed: 'left' | 'right' | undefined = props.config.fixed === false ? undefined : 'left';
116
-
105
+ const defaultFixed: 'left' | 'right' = isGlobalFlat.value ? 'right' : 'left';
106
+ let actionFixed: 'left' | 'right' | undefined = props.config.fixed === false ? undefined : defaultFixed;
117
107
  if (typeof props.config.fixed === 'string' && ['left', 'right'].includes(props.config.fixed)) {
118
108
  actionFixed = props.config.fixed;
119
109
  }
120
110
 
111
+ const actionFlat = props.config.flat === undefined && isGlobalFlat.value ? true : props.config.flat;
112
+
121
113
  const actionColumn = {
122
114
  props: {
123
115
  label: '操作',
124
116
  fixed: actionFixed,
117
+
125
118
  width: props.config.operateColWidth ?? (props.config.dropSortHandle && props.config.dropSort ? 132 : 112),
126
119
  align: 'center',
127
120
  },
128
121
  cell: ({ row, $index }: any) =>
129
122
  h(ActionsColumn, {
130
123
  row,
131
- flat: props.config.flat,
124
+ flat: actionFlat,
132
125
  index: $index,
133
126
  model: props.model,
134
127
  config: props.config,
@@ -182,7 +175,7 @@ export const useTableColumns = (
182
175
  }
183
176
 
184
177
  for (const column of props.config.items) {
185
- if (column.type !== 'hidden' && display(column.display)) {
178
+ if (isTableColumnRendered(column, display)) {
186
179
  const titleTipValue = titleTip(column.titleTip);
187
180
 
188
181
  columns.push({
@@ -200,7 +193,7 @@ export const useTableColumns = (
200
193
  disabled: props.disabled,
201
194
  prop: getProp($index),
202
195
  rules: column.rules,
203
- config: makeConfig(column, row) as FormItemConfig,
196
+ config: makeTableColumnConfig(column, row) as FormItemConfig,
204
197
  model: row,
205
198
  lastValues: lastData.value[$index],
206
199
  isCompare: props.isCompare,
@@ -12,6 +12,7 @@
12
12
  :last-values="lastValues"
13
13
  :prop="prop"
14
14
  :label-width="labelWidth"
15
+ :label-position="labelPosition"
15
16
  :show-index="showIndex"
16
17
  :sort-key="sortKey"
17
18
  :sort="sort"
@@ -20,13 +21,8 @@
20
21
  @addDiffCount="onAddDiffCount"
21
22
  @add="onAdd"
22
23
  >
23
- <template #toggle-button>
24
- <TMagicButton
25
- v-if="config.enableToggleMode || enableToggleMode"
26
- :icon="Grid"
27
- size="small"
28
- @click="toggleDisplayMode"
29
- >
24
+ <template #toggle-button v-if="config.enableToggleMode || enableToggleMode">
25
+ <TMagicButton :icon="Grid" size="small" @click="toggleDisplayMode">
30
26
  {{ displayMode === 'table' ? '展开配置' : '切换为表格' }}
31
27
  </TMagicButton>
32
28
  </template>
@@ -35,19 +31,23 @@
35
31
  <TMagicButton
36
32
  :class="displayMode === 'table' ? 'm-form-table-add-button' : ''"
37
33
  :size="addButtonSize"
38
- :plain="displayMode === 'table'"
39
34
  :icon="Plus"
40
- text
41
35
  :disabled="disabled"
42
- v-bind="currentConfig.addButtonConfig?.props || { type: 'primary' }"
43
- @click="newHandler"
36
+ v-bind="addButtonProps"
37
+ @click="handleAdd"
44
38
  >
45
39
  {{
46
40
  currentConfig.addButtonConfig?.text ||
47
- (displayMode === 'table' ? '新增一行' : `新增${groupListConfig.titlePrefix || ''}`)
41
+ (displayMode === 'table'
42
+ ? `新增一行${groupListConfig.titlePrefix || ''}`
43
+ : `新增${groupListConfig.titlePrefix || ''}`)
48
44
  }}
49
45
  </TMagicButton>
50
46
  </template>
47
+
48
+ <template #title="slotProps" v-if="$slots.title">
49
+ <slot name="title" v-bind="slotProps"></slot>
50
+ </template>
51
51
  </component>
52
52
  </template>
53
53
 
@@ -58,11 +58,14 @@ import { Grid, Plus } from '@element-plus/icons-vue';
58
58
  import { TMagicButton } from '@tmagic/design';
59
59
  import type { GroupListConfig, TableConfig } from '@tmagic/form-schema';
60
60
 
61
- import type { ContainerChangeEventData } from '../../schema';
61
+ import type { ContainerChangeEventData } from '@form/schema';
62
+ import { isGroupListType, toGroupListConfig, toTableConfig } from '@form/utils/tableGroupList';
63
+
62
64
  import MFormGroupList from '../GroupList.vue';
63
65
  import MFormTable from '../table/Table.vue';
64
66
 
65
67
  import { useAdd } from './useAdd';
68
+ import { useScrollLastItemIntoView } from './useScrollLastItemIntoView';
66
69
 
67
70
  defineOptions({
68
71
  name: 'MFormTableGroupList',
@@ -77,6 +80,7 @@ const props = defineProps<{
77
80
  name: string;
78
81
  prop?: string;
79
82
  labelWidth?: string;
83
+ labelPosition?: 'top' | 'left' | 'right';
80
84
  disabled?: boolean;
81
85
  size?: string;
82
86
  enableToggleMode?: true;
@@ -89,88 +93,58 @@ const emit = defineEmits(['change', 'select', 'addDiffCount']);
89
93
 
90
94
  const { addable, newHandler } = useAdd(props, emit);
91
95
 
92
- const isGroupListType = (type: string | undefined) => type === 'groupList' || type === 'group-list';
93
-
94
96
  const displayMode = ref<'table' | 'groupList'>(isGroupListType(props.config.type) ? 'groupList' : 'table');
95
97
 
96
- const calcLabelWidth = (label: string) => {
97
- if (!label) return '0px';
98
- const zhLength = label.match(/[^\x00-\xff]/g)?.length || 0;
99
- const chLength = label.length - zhLength;
100
- return `${Math.max(chLength * 8 + zhLength * 20, 80)}px`;
101
- };
102
-
103
98
  // 当原始 config 是 table 形态时,table 模式直接透传;
104
- // 若原始是 groupList,则基于它派生出 table 所需的 config
105
- const tableConfig = computed<TableConfig>(() => {
106
- if (!isGroupListType(props.config.type)) {
107
- return props.config as TableConfig;
108
- }
109
-
110
- const source = props.config as GroupListConfig;
111
- return {
112
- ...props.config,
113
- type: 'table',
114
- groupItems: source.items,
115
- items:
116
- source.tableItems ||
117
- (source.items as any[]).map((item: any) => ({
118
- ...item,
119
- label: item.label || item.text,
120
- text: null,
121
- })),
122
- } as any as TableConfig;
123
- });
124
-
125
- // 反向派生 groupList 所需的 config
126
- const groupListConfig = computed<GroupListConfig>(() => {
127
- if (isGroupListType(props.config.type)) {
128
- return props.config as GroupListConfig;
129
- }
99
+ // 若原始是 groupList,则基于它派生出 table 所需的 config(反之同理)
100
+ const tableConfig = computed<TableConfig>(() => toTableConfig(props.config));
130
101
 
131
- const source = props.config as TableConfig;
132
- return {
133
- ...props.config,
134
- type: 'groupList',
135
- tableItems: source.items,
136
- items:
137
- source.groupItems ||
138
- (source.items as any[]).map((item: any) => {
139
- const text = item.text || item.label;
140
- return {
141
- ...item,
142
- text,
143
- labelWidth: calcLabelWidth(text),
144
- span: item.span || 12,
145
- };
146
- }),
147
- } as any as GroupListConfig;
148
- });
102
+ const groupListConfig = computed<GroupListConfig>(() => toGroupListConfig(props.config));
149
103
 
150
104
  // 运行时类型由 displayMode 决定,`<component :is>` 无法做联合类型收窄,统一转 any 交给子组件处理
151
105
  const currentConfig = computed<any>(() => (displayMode.value === 'table' ? tableConfig.value : groupListConfig.value));
152
106
 
153
107
  // 保持原 Table/GroupList 模式下新增按钮的不同尺寸策略
154
108
  const addButtonSize = computed(() => {
109
+ if (currentConfig.value.addButtonConfig?.sticky) return 'default';
155
110
  if (displayMode.value === 'table') return 'small';
156
111
  return props.config.enableToggleMode !== false ? 'small' : 'default';
157
112
  });
158
113
 
114
+ const addButtonProps = computed(() => {
115
+ const custom = currentConfig.value.addButtonConfig?.props;
116
+ if (custom) return { type: 'primary', ...custom };
117
+ if (displayMode.value === 'table') return { type: 'primary', plain: true };
118
+ return { type: 'primary', text: true };
119
+ });
120
+
159
121
  const toggleDisplayMode = () => {
160
122
  displayMode.value = displayMode.value === 'table' ? 'groupList' : 'table';
161
123
  };
162
124
 
125
+ const tableGroupListRef = useTemplateRef<InstanceType<typeof MFormTable>>('tableGroupList');
126
+
127
+ const { scrollLastItemIntoView } = useScrollLastItemIntoView(
128
+ tableGroupListRef,
129
+ () => Boolean(currentConfig.value.scrollLastItemIntoView) && displayMode.value === 'groupList',
130
+ );
131
+
132
+ const handleAdd = async () => {
133
+ const expectedCount = await newHandler();
134
+ if (expectedCount !== null) await scrollLastItemIntoView(expectedCount);
135
+ };
136
+
163
137
  const onChange = (v: any, eventData?: ContainerChangeEventData) => emit('change', v, eventData);
164
138
  const onSelect = (...args: any[]) => emit('select', ...args);
165
139
  const onAddDiffCount = () => emit('addDiffCount');
166
- const onAdd = (rows: any[]) => {
167
- rows.forEach((row: any) => {
168
- newHandler(row);
169
- });
140
+ const onAdd = async (rows: any[]) => {
141
+ let expectedCount: number | null = null;
142
+ for (const row of rows) {
143
+ expectedCount = (await newHandler(row)) ?? expectedCount;
144
+ }
145
+ if (expectedCount !== null) await scrollLastItemIntoView(expectedCount);
170
146
  };
171
147
 
172
- const tableGroupListRef = useTemplateRef<InstanceType<typeof MFormTable>>('tableGroupList');
173
-
174
148
  defineExpose({
175
149
  toggleRowSelection: (row: any, selected: boolean) => tableGroupListRef.value?.toggleRowSelection?.(row, selected),
176
150
  });
@@ -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
+ };