@tmagic/form 1.8.0-beta.1 → 1.8.0-beta.3

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.
package/src/Form.vue CHANGED
@@ -22,7 +22,11 @@
22
22
  :step-active="stepActive"
23
23
  :size="size"
24
24
  @change="changeHandler"
25
- ></Container>
25
+ >
26
+ <template v-if="$slots.label" #label="labelProps">
27
+ <slot name="label" v-bind="labelProps"></slot>
28
+ </template>
29
+ </Container>
26
30
  </template>
27
31
  </TMagicForm>
28
32
  </template>
@@ -37,12 +41,23 @@ import { setValueByKeyPath } from '@tmagic/utils';
37
41
  import Container from './containers/Container.vue';
38
42
  import { getConfig } from './utils/config';
39
43
  import { initValue } from './utils/form';
40
- import type { ChangeRecord, ContainerChangeEventData, FormConfig, FormState, FormValue, ValidateError } from './schema';
44
+ import type {
45
+ ChangeRecord,
46
+ ContainerChangeEventData,
47
+ FormConfig,
48
+ FormSlots,
49
+ FormState,
50
+ FormValue,
51
+ ValidateError,
52
+ } from './schema';
53
+ import { FORM_DIFF_CONFIG_KEY } from './schema';
41
54
 
42
55
  defineOptions({
43
56
  name: 'MForm',
44
57
  });
45
58
 
59
+ defineSlots<FormSlots>();
60
+
46
61
  const props = withDefaults(
47
62
  defineProps<{
48
63
  /** 表单配置 */
@@ -65,6 +80,33 @@ const props = withDefaults(
65
80
  popperClass?: string;
66
81
  preventSubmitDefault?: boolean;
67
82
  extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
83
+ /**
84
+ * 自定义"是否展示对比内容"的判断函数(仅在 `isCompare === true` 时生效)。
85
+ *
86
+ * - 不传:使用默认逻辑 `!isEqual(curValue, lastValue)`;
87
+ * - 传函数:完全以函数返回值为准,返回 `true` 才展示前后两份对比内容。
88
+ *
89
+ * 通过 provide 下发给所有层级的 Container(含嵌套在容器组件内部的 Container),
90
+ * 调用方只需在 MForm 这一层传一次即可对整棵表单生效。
91
+ *
92
+ * 典型场景:某些字段语义上相等但结构不同(例如 `code-select` 字段中 `''` 与
93
+ * `{ hookType: 'code', hookData: [] }` 应视为相等),调用方在此处显式声明,
94
+ * 避免被 lodash `isEqual` 误判为差异。
95
+ */
96
+ showDiff?: (_data: { curValue: any; lastValue: any; config: any }) => boolean;
97
+ /**
98
+ * 自定义「自接管对比」的字段类型(仅在对比模式下生效)。
99
+ *
100
+ * 自接管对比的字段不会渲染前后两份独立组件,而是只渲染一次并由字段组件内部展示前后差异
101
+ * (如 vs-code 使用 monaco diff 编辑器;event-select / code-select-col 等复合字段逐项展示差异)。
102
+ *
103
+ * 支持两种形式:
104
+ * - 传数组:在内置类型基础上「追加」这些类型;
105
+ * - 传函数:入参为内置类型数组,返回值作为「最终」完整列表(可完全替换内置项)。
106
+ *
107
+ * 通过 provide 下发,对整棵表单的所有层级 Container 生效,只需在 MForm 这一层传一次。
108
+ */
109
+ selfDiffFieldTypes?: string[] | ((_defaultTypes: string[]) => string[]);
68
110
  }>(),
69
111
  {
70
112
  config: () => [],
@@ -201,6 +243,17 @@ watchEffect(async (onCleanup) => {
201
243
 
202
244
  provide('mForm', formState);
203
245
 
246
+ // 对比相关配置单独通过 provide 下发,所有层级的 Container 通过 inject 获取,无需逐层透传 prop。
247
+ // 用 getter 对象保证读取时回到最新的 props 值,维持响应式。
248
+ provide(FORM_DIFF_CONFIG_KEY, {
249
+ get showDiff() {
250
+ return props.showDiff;
251
+ },
252
+ get selfDiffFieldTypes() {
253
+ return props.selfDiffFieldTypes;
254
+ },
255
+ });
256
+
204
257
  const changeRecords = shallowRef<ChangeRecord[]>([]);
205
258
 
206
259
  watch(
@@ -25,13 +25,15 @@
25
25
  <template v-else-if="type && display && !showDiff">
26
26
  <TMagicFormItem v-bind="formItemProps" :class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text }">
27
27
  <template #label>
28
- <FormLabel
29
- :tip="config.tip"
30
- :type="type"
31
- :use-label="(config as CheckboxConfig).useLabel"
32
- :label-title="config.labelTitle"
33
- :text="text"
34
- ></FormLabel>
28
+ <slot name="label" :config="config" :type="type" :text="text" :prop="itemProp" :disabled="disabled">
29
+ <FormLabel
30
+ :tip="config.tip"
31
+ :type="type"
32
+ :use-label="(config as CheckboxConfig).useLabel"
33
+ :label-title="config.labelTitle"
34
+ :text="text"
35
+ ></FormLabel>
36
+ </slot>
35
37
  </template>
36
38
 
37
39
  <TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
@@ -71,68 +73,126 @@
71
73
 
72
74
  <!-- 对比 -->
73
75
  <template v-else-if="type && display && showDiff">
74
- <!-- 上次内容 -->
76
+ <!-- 自接管对比的字段类型(如 vs-code):只渲染一次组件,由字段内部用 diff 编辑器/视图自行展示前后差异 -->
75
77
  <TMagicFormItem
78
+ v-if="isSelfDiffField"
76
79
  v-bind="formItemProps"
77
- :class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text, 'show-diff': true }"
80
+ :class="{
81
+ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text,
82
+ 'self-diff': true,
83
+ }"
78
84
  >
79
85
  <template #label>
80
- <FormLabel
81
- :tip="config.tip"
82
- :type="type"
83
- :use-label="(config as CheckboxConfig).useLabel"
84
- :label-title="config.labelTitle"
85
- :text="text"
86
- ></FormLabel>
86
+ <slot name="label" :config="config" :type="type" :text="text" :prop="itemProp" :disabled="disabled">
87
+ <FormLabel
88
+ :tip="config.tip"
89
+ :type="type"
90
+ :use-label="(config as CheckboxConfig).useLabel"
91
+ :label-title="config.labelTitle"
92
+ :text="text"
93
+ ></FormLabel>
94
+ </slot>
87
95
  </template>
88
96
  <TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
89
- <component v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
97
+ <component
98
+ v-bind="fieldsProps"
99
+ :is="tagName"
100
+ :model="model"
101
+ :last-values="lastValues"
102
+ :is-compare="isCompare"
103
+ @change="onChangeHandler"
104
+ ></component>
90
105
  <template #content>
91
106
  <div v-html="tooltip.text"></div>
92
107
  </template>
93
108
  </TMagicTooltip>
94
109
 
95
- <component v-else v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
110
+ <component
111
+ v-else
112
+ v-bind="fieldsProps"
113
+ :is="tagName"
114
+ :model="model"
115
+ :last-values="lastValues"
116
+ :is-compare="isCompare"
117
+ @change="onChangeHandler"
118
+ ></component>
96
119
  </TMagicFormItem>
97
120
 
98
- <TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
99
- <TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
100
- <template #content>
101
- <div v-html="config.tip"></div>
102
- </template>
103
- </TMagicTooltip>
121
+ <!-- 普通字段:渲染前后两份独立的组件用于对比 -->
122
+ <template v-else>
123
+ <!-- 上次内容 -->
124
+ <TMagicFormItem
125
+ v-bind="formItemProps"
126
+ :class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text, 'show-before-diff': true }"
127
+ >
128
+ <template #label>
129
+ <slot name="label" :config="config" :type="type" :text="text" :prop="itemProp" :disabled="disabled">
130
+ <FormLabel
131
+ :tip="config.tip"
132
+ :type="type"
133
+ :use-label="(config as CheckboxConfig).useLabel"
134
+ :label-title="config.labelTitle"
135
+ :text="text"
136
+ ></FormLabel>
137
+ </slot>
138
+ </template>
139
+ <TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
140
+ <component v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
141
+ <template #content>
142
+ <div v-html="tooltip.text"></div>
143
+ </template>
144
+ </TMagicTooltip>
104
145
 
105
- <!-- 当前内容 -->
106
- <TMagicFormItem
107
- v-bind="formItemProps"
108
- :style="config.tip ? 'flex: 1' : ''"
109
- :class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text, 'show-diff': true }"
110
- >
111
- <template #label>
112
- <FormLabel
113
- :tip="config.tip"
114
- :type="type"
115
- :use-label="(config as CheckboxConfig).useLabel"
116
- :label-title="config.labelTitle"
117
- :text="text"
118
- ></FormLabel>
119
- </template>
120
- <TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
121
- <component v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
146
+ <component
147
+ v-else
148
+ v-bind="fieldsProps"
149
+ :is="tagName"
150
+ :model="lastValues"
151
+ @change="onChangeHandler"
152
+ ></component>
153
+ </TMagicFormItem>
154
+
155
+ <TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
156
+ <TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
122
157
  <template #content>
123
- <div v-html="tooltip.text"></div>
158
+ <div v-html="config.tip"></div>
124
159
  </template>
125
160
  </TMagicTooltip>
126
161
 
127
- <component v-else v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
128
- </TMagicFormItem>
129
-
130
- <TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
131
- <TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
132
- <template #content>
133
- <div v-html="config.tip"></div>
134
- </template>
135
- </TMagicTooltip>
162
+ <!-- 当前内容 -->
163
+ <TMagicFormItem
164
+ v-bind="formItemProps"
165
+ :style="config.tip ? 'flex: 1' : ''"
166
+ :class="{ 'tmagic-form-hidden': `${itemLabelWidth}` === '0' || !text, 'show-after-diff': true }"
167
+ >
168
+ <template #label>
169
+ <slot name="label" :config="config" :type="type" :text="text" :prop="itemProp" :disabled="disabled">
170
+ <FormLabel
171
+ :tip="config.tip"
172
+ :type="type"
173
+ :use-label="(config as CheckboxConfig).useLabel"
174
+ :label-title="config.labelTitle"
175
+ :text="text"
176
+ ></FormLabel>
177
+ </slot>
178
+ </template>
179
+ <TMagicTooltip v-if="tooltip.text" :placement="tooltip.placement">
180
+ <component v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
181
+ <template #content>
182
+ <div v-html="tooltip.text"></div>
183
+ </template>
184
+ </TMagicTooltip>
185
+
186
+ <component v-else v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
187
+ </TMagicFormItem>
188
+
189
+ <TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
190
+ <TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
191
+ <template #content>
192
+ <div v-html="config.tip"></div>
193
+ </template>
194
+ </TMagicTooltip>
195
+ </template>
136
196
  </template>
137
197
 
138
198
  <template v-else-if="items && display">
@@ -152,7 +212,11 @@
152
212
  :prop="itemProp"
153
213
  @change="onChangeHandler"
154
214
  @addDiffCount="onAddDiffCount"
155
- ></Container>
215
+ >
216
+ <template v-if="$slots.label" #label="labelProps">
217
+ <slot name="label" v-bind="labelProps"></slot>
218
+ </template>
219
+ </Container>
156
220
  </template>
157
221
  </template>
158
222
 
@@ -178,11 +242,14 @@ import type {
178
242
  ComponentConfig,
179
243
  ContainerChangeEventData,
180
244
  ContainerCommonConfig,
245
+ FormDiffConfig,
181
246
  FormItemConfig,
247
+ FormSlots,
182
248
  FormState,
183
249
  FormValue,
184
250
  ToolTipConfigType,
185
251
  } from '../schema';
252
+ import { FORM_DIFF_CONFIG_KEY } from '../schema';
186
253
  import { getField } from '../utils/config';
187
254
  import { createObjectProp, display as displayFunction, filterFunction, getRules } from '../utils/form';
188
255
 
@@ -192,6 +259,8 @@ defineOptions({
192
259
  name: 'MFormContainer',
193
260
  });
194
261
 
262
+ defineSlots<FormSlots>();
263
+
195
264
  const props = withDefaults(
196
265
  defineProps<{
197
266
  /** 表单值 */
@@ -224,14 +293,30 @@ const emit = defineEmits<{
224
293
 
225
294
  const mForm = inject<FormState | undefined>('mForm');
226
295
 
296
+ // 对比相关配置由 MForm 通过 provide 下发,这里直接 inject,无需逐层透传 prop。
297
+ const diffConfig = inject<FormDiffConfig>(FORM_DIFF_CONFIG_KEY, {});
298
+
227
299
  const expand = ref(false);
228
300
 
229
301
  const name = computed(() => props.config.name || '');
302
+
230
303
  // 是否展示两个版本的对比内容
304
+ //
305
+ // 默认逻辑:在对比模式下用 lodash isEqual 比较当前值与历史值,不相等则展示对比。
306
+ // 若调用方通过 MForm 的 `showDiff` 注入了自定义判断函数,则完全以其返回值为准,
307
+ // 便于业务侧自定义"语义上相等"的特殊场景(例如空字符串与空 hook 结构)。
231
308
  const showDiff = computed(() => {
232
309
  if (!props.isCompare) return false;
233
- const curValue = name.value ? props.model[name.value] : props.model;
234
- const lastValue = name.value ? props.lastValues[name.value] : props.lastValues;
310
+ if (!name.value) return false;
311
+
312
+ const curValue = props.model[name.value];
313
+ const lastValue = props.lastValues[name.value];
314
+
315
+ const customShowDiff = diffConfig.showDiff;
316
+ if (typeof customShowDiff === 'function') {
317
+ return Boolean(customShowDiff({ curValue, lastValue, config: props.config }));
318
+ }
319
+
235
320
  return !isEqual(curValue, lastValue);
236
321
  });
237
322
 
@@ -268,6 +353,45 @@ const tagName = computed(() => {
268
353
  return getField(type.value || 'container') || `m-${items.value ? 'form' : 'fields'}-${type.value}`;
269
354
  });
270
355
 
356
+ /**
357
+ * 自接管对比的字段类型白名单。
358
+ *
359
+ * 这类字段在 `isCompare === true` 且存在差异时,不再由 Container 渲染前后两份独立组件来对比,
360
+ * 而是只渲染一次组件,将 `model` / `lastValues` / `isCompare` 一并传给字段组件,
361
+ * 由字段组件内部自行展示前后差异(典型场景:vs-code 字段使用 monaco 自带的 diff 编辑器)。
362
+ *
363
+ * 这样做的好处:
364
+ * 1. 避免重型字段(如 monaco 编辑器)在对比模式下被实例化两次,节省资源;
365
+ * 2. 提供更专业的对比视觉效果(如 monaco diff 的行级高亮、左右滚动同步等)。
366
+ *
367
+ * 注意:像 `event-select` / `code-select-col` 这类内部由列表 / 嵌套子表单组成的复合字段,若按默认逻辑
368
+ * 渲染前后两份独立组件,会出现两套下拉框 + 两份参数表单(或两套「添加事件」按钮、两份完整面板),
369
+ * 体验很差。这类字段在内部把 `is-compare`/`lastValues` 透传给子级容器,由子级逐项展示差异,
370
+ * 因此同样归类为自接管对比字段。
371
+ */
372
+ const DEFAULT_SELF_DIFF_FIELD_TYPES = ['vs-code', 'event-select', 'code-select-col', 'code-select'];
373
+
374
+ // 最终生效的自接管对比字段类型集合。
375
+ //
376
+ // - 未自定义:使用内置默认类型;
377
+ // - 自定义传数组:在内置类型基础上「追加」;
378
+ // - 自定义传函数:以函数返回值为「最终」完整列表(可完全替换内置项)。
379
+ const effectiveSelfDiffFieldTypes = computed<Set<string>>(() => {
380
+ const custom = diffConfig.selfDiffFieldTypes;
381
+
382
+ if (typeof custom === 'function') {
383
+ return new Set(custom([...DEFAULT_SELF_DIFF_FIELD_TYPES]));
384
+ }
385
+
386
+ if (Array.isArray(custom)) {
387
+ return new Set([...DEFAULT_SELF_DIFF_FIELD_TYPES, ...custom]);
388
+ }
389
+
390
+ return new Set(DEFAULT_SELF_DIFF_FIELD_TYPES);
391
+ });
392
+
393
+ const isSelfDiffField = computed(() => effectiveSelfDiffFieldTypes.value.has(type.value));
394
+
271
395
  const disabled = computed(() => props.disabled || filterFunction(mForm, props.config.disabled, props));
272
396
 
273
397
  const text = computed(() => filterFunction(mForm, props.config.text, props));
@@ -7,11 +7,11 @@
7
7
  :true-value="checkboxTrueValue"
8
8
  :false-value="checkboxFalseValue"
9
9
  @update:modelValue="valueChangeHandler"
10
- ><span v-html="config.legend"></span><span v-if="config.extra" v-html="config.extra" class="m-form-tip"></span
10
+ ><span v-html="legend"></span><span v-if="config.extra" v-html="config.extra" class="m-form-tip"></span
11
11
  ></TMagicCheckbox>
12
12
  </component>
13
13
  <legend v-else>
14
- <span v-html="config.legend"></span>
14
+ <span v-html="legend"></span>
15
15
  <span v-if="config.extra" v-html="config.extra" class="m-form-tip"></span>
16
16
  </legend>
17
17
 
@@ -63,6 +63,7 @@ import { computed, inject } from 'vue';
63
63
  import { TMagicCheckbox } from '@tmagic/design';
64
64
 
65
65
  import { ContainerChangeEventData, FieldsetConfig, FormState } from '../schema';
66
+ import { filterFunction } from '../utils/form';
66
67
 
67
68
  import Container from './Container.vue';
68
69
 
@@ -99,6 +100,8 @@ const mForm = inject<FormState | undefined>('mForm');
99
100
 
100
101
  const name = computed(() => props.config.name || '');
101
102
 
103
+ const legend = computed(() => filterFunction<string>(mForm, props.config.legend, props));
104
+
102
105
  const checkboxName = computed(() => {
103
106
  if (typeof props.config.checkbox === 'object' && typeof props.config.checkbox.name === 'string') {
104
107
  return props.config.checkbox.name;
@@ -26,7 +26,7 @@
26
26
  @addDiffCount="onAddDiffCount()"
27
27
  ></MFieldsGroupListItem>
28
28
 
29
- <div class="m-fields-group-list-footer">
29
+ <div class="m-fields-group-list-footer" v-if="!isCompare">
30
30
  <slot name="toggle-button"></slot>
31
31
  <div style="display: flex; justify-content: flex-end; flex: 1">
32
32
  <slot name="add-button"></slot>
@@ -7,6 +7,7 @@
7
7
  </TMagicButton>
8
8
 
9
9
  <TMagicButton
10
+ v-if="!isCompare"
10
11
  v-show="showDelete"
11
12
  type="danger"
12
13
  size="small"
@@ -17,7 +18,7 @@
17
18
  ></TMagicButton>
18
19
 
19
20
  <TMagicButton
20
- v-if="copyable"
21
+ v-if="copyable && !isCompare"
21
22
  link
22
23
  size="small"
23
24
  type="primary"
@@ -27,7 +28,7 @@
27
28
  >复制</TMagicButton
28
29
  >
29
30
 
30
- <template v-if="movable">
31
+ <template v-if="movable && !isCompare">
31
32
  <TMagicButton
32
33
  v-show="index !== 0"
33
34
  link
@@ -49,7 +50,7 @@
49
50
  </template>
50
51
 
51
52
  <TMagicPopover
52
- v-if="config.moveSpecifyLocation"
53
+ v-if="config.moveSpecifyLocation && !isCompare"
53
54
  trigger="click"
54
55
  placement="top"
55
56
  width="200"
@@ -19,7 +19,10 @@
19
19
  :is="tabPaneComponent?.component || 'el-tab-pane'"
20
20
  :key="tab[mForm?.keyProp || '__key'] ?? tabIndex"
21
21
  v-bind="
22
- tabPaneComponent?.props({ name: filter(tab.status) || tabIndex.toString(), lazy: tab.lazy || false }) || {}
22
+ tabPaneComponent?.props({
23
+ name: filter(tab.status) || tabIndex.toString(),
24
+ lazy: isCompare ? false : tab.lazy || false,
25
+ }) || {}
23
26
  "
24
27
  >
25
28
  <template #label>
@@ -76,7 +79,7 @@
76
79
  </template>
77
80
 
78
81
  <script setup lang="ts">
79
- import { computed, inject, ref, watchEffect } from 'vue';
82
+ import { computed, inject, ref, watch, watchEffect } from 'vue';
80
83
  import { isEmpty } from 'lodash-es';
81
84
 
82
85
  import { getDesignConfig, TMagicBadge } from '@tmagic/design';
@@ -172,6 +175,11 @@ watchEffect(() => {
172
175
  }
173
176
  });
174
177
 
178
+ // model 或 lastValues 变化时,重置差异数
179
+ watch([() => props.model, () => props.lastValues], () => {
180
+ diffCount.value = {};
181
+ });
182
+
175
183
  const tabItems = (tab: TabPaneConfig) => (props.config.dynamic ? props.config.items : tab.items);
176
184
 
177
185
  const tabClickHandler = (tab: any) => {
@@ -44,7 +44,7 @@
44
44
  {{ isFullscreen ? '退出全屏' : '全屏编辑' }}
45
45
  </TMagicButton>
46
46
  <TMagicUpload
47
- v-if="importable"
47
+ v-if="importable && !isCompare"
48
48
  style="display: inline-block"
49
49
  ref="excelBtn"
50
50
  action="/noop"
@@ -54,11 +54,17 @@
54
54
  >
55
55
  <TMagicButton size="small" type="success" :disabled="disabled" plain>导入EXCEL</TMagicButton>
56
56
  </TMagicUpload>
57
- <TMagicButton v-if="importable" size="small" type="warning" :disabled="disabled" plain @click="clearHandler"
57
+ <TMagicButton
58
+ v-if="importable && !isCompare"
59
+ size="small"
60
+ type="warning"
61
+ :disabled="disabled"
62
+ plain
63
+ @click="clearHandler"
58
64
  >清空</TMagicButton
59
65
  >
60
66
  </div>
61
- <slot name="add-button"></slot>
67
+ <slot name="add-button" v-if="!isCompare"></slot>
62
68
  </div>
63
69
 
64
70
  <div class="bottom" style="text-align: right" v-if="config.pagination">
package/src/schema.ts CHANGED
@@ -1,5 +1,33 @@
1
+ import type { InjectionKey } from 'vue';
2
+
3
+ import type { FormItemConfig } from '@tmagic/form-schema';
4
+
1
5
  export * from '@tmagic/form-schema';
2
6
 
7
+ /**
8
+ * 对比模式相关配置,由 `MForm` 通过 `provide` 注入,
9
+ * 所有层级的 Container(含嵌套在 fieldset / panel 等容器组件内部的 Container)通过 `inject` 获取,
10
+ * 无需逐层透传 prop。
11
+ */
12
+ export interface FormDiffConfig {
13
+ /**
14
+ * 自定义"是否展示对比内容"的判断函数(仅在对比模式下生效)。
15
+ *
16
+ * - 不传:使用默认逻辑 `!isEqual(curValue, lastValue)`;
17
+ * - 传函数:完全以函数返回值为准,返回 `true` 才展示前后两份对比内容。
18
+ */
19
+ showDiff?: (_data: { curValue: any; lastValue: any; config: FormItemConfig }) => boolean;
20
+ /**
21
+ * 自定义「自接管对比」的字段类型(仅在对比模式下生效)。
22
+ *
23
+ * - 传数组:在内置类型基础上「追加」这些类型;
24
+ * - 传函数:入参为内置类型数组,返回值作为「最终」完整类型列表(可完全替换内置项)。
25
+ */
26
+ selfDiffFieldTypes?: string[] | ((_defaultTypes: string[]) => string[]);
27
+ }
28
+
29
+ export const FORM_DIFF_CONFIG_KEY: InjectionKey<FormDiffConfig> = Symbol('mFormDiffConfig');
30
+
3
31
  export interface ValidateError {
4
32
  message: string;
5
33
  field: string;
@@ -14,3 +42,22 @@ export interface ContainerChangeEventData {
14
42
  modifyKey?: string;
15
43
  changeRecords?: ChangeRecord[];
16
44
  }
45
+
46
+ /** 自定义 label slot 的作用域参数 */
47
+ export interface FormLabelSlotProps {
48
+ /** 当前表单项配置 */
49
+ config: FormItemConfig;
50
+ /** 经处理后的类型 */
51
+ type: string;
52
+ /** 经 filterFunction 处理后的 label 文案 */
53
+ text?: string;
54
+ /** 完整字段路径(包含父级前缀) */
55
+ prop: string;
56
+ /** 经 filterFunction 处理后的最终禁用状态 */
57
+ disabled?: boolean;
58
+ }
59
+
60
+ /** Form / Container 暴露的具名 slot 定义 */
61
+ export interface FormSlots {
62
+ label(_props: FormLabelSlotProps): any;
63
+ }
package/src/submitForm.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  import { type AppContext, type Component, createApp, defineComponent, h, nextTick, ref, watch } from 'vue';
20
20
 
21
21
  import Form from './Form.vue';
22
- import type { FormConfig, FormState } from './schema';
22
+ import type { ChangeRecord, FormConfig, FormState } from './schema';
23
23
 
24
24
  // #region SubmitFormOptions
25
25
  /**
@@ -48,6 +48,11 @@ export interface SubmitFormOptions {
48
48
  extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
49
49
  /** 透传给 Form.submitForm 的参数:是否直接返回原始响应式 values */
50
50
  native?: boolean;
51
+ /**
52
+ * 是否在 resolve 结果中携带 changeRecords(变更记录)。
53
+ * 开启后 resolve 的结果为 `{ values, changeRecords }`,否则仅 resolve values。
54
+ */
55
+ returnChangeRecords?: boolean;
51
56
  /**
52
57
  * 父级应用上下文,用于继承全局组件、指令、provide 等。
53
58
  * 通常通过 `app._context` 或 `getCurrentInstance()?.appContext` 获取。
@@ -58,6 +63,18 @@ export interface SubmitFormOptions {
58
63
  }
59
64
  // #endregion SubmitFormOptions
60
65
 
66
+ // #region SubmitFormResult
67
+ /**
68
+ * 开启 `returnChangeRecords` 时 submitForm 的返回结果
69
+ */
70
+ export interface SubmitFormResult {
71
+ /** 校验通过后的表单值 */
72
+ values: any;
73
+ /** 表单变更记录 */
74
+ changeRecords: ChangeRecord[];
75
+ }
76
+ // #endregion SubmitFormResult
77
+
61
78
  /**
62
79
  * 以命令式方式调用 Form.vue 完成一次表单校验/提交。
63
80
  *
@@ -78,10 +95,17 @@ export interface SubmitFormOptions {
78
95
  * } catch (e) {
79
96
  * console.error(e);
80
97
  * }
98
+ *
99
+ * // 需要同时获取变更记录时:
100
+ * const { values, changeRecords } = await submitForm({
101
+ * config: [...],
102
+ * initValues: { name: 'foo' },
103
+ * returnChangeRecords: true,
104
+ * });
81
105
  * ```
82
106
  */
83
107
  export const submitForm = (options: SubmitFormOptions): Promise<any> => {
84
- const { native, appContext, timeout = 10000, ...formProps } = options;
108
+ const { native, appContext, timeout = 10000, returnChangeRecords, ...formProps } = options;
85
109
 
86
110
  return new Promise((resolve, reject) => {
87
111
  const container = document.createElement('div');
@@ -105,8 +129,10 @@ export const submitForm = (options: SubmitFormOptions): Promise<any> => {
105
129
  try {
106
130
  // 等待子组件(FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
107
131
  await nextTick();
132
+ // submitForm 校验通过后会清空 changeRecords,需在调用前先做快照
133
+ const changeRecords: ChangeRecord[] = [...(formRef.value.changeRecords ?? [])];
108
134
  const result = await formRef.value.submitForm(native);
109
- resolve(result);
135
+ resolve(returnChangeRecords ? { values: result, changeRecords } : result);
110
136
  } catch (err) {
111
137
  reject(err);
112
138
  } finally {