@tmagic/form 1.8.0-beta.1 → 1.8.0-beta.11
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/dist/es/Form.vue_vue_type_script_setup_true_lang.js +25 -6
- package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +195 -78
- package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +5 -3
- package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/FormLabel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +10 -9
- package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/containers/table/ActionsColumn.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/table/SortColumn.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Checkbox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/CheckboxGroup.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/ColorPicker.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Date.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/DateTime.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Daterange.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Display.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/DynamicField.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Hidden.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/NumberRange.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/RadioGroup.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Switch.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Textarea.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Time.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Timerange.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/index.js +2 -1
- package/dist/es/schema.js +4 -0
- package/dist/es/style.css +4 -1
- package/dist/es/submitForm.js +111 -8
- package/dist/es/utils/form.js +2 -2
- package/dist/style.css +4 -1
- package/dist/tmagic-form.umd.cjs +1865 -1598
- package/package.json +5 -5
- package/src/Form.vue +64 -3
- package/src/FormBox.vue +4 -0
- package/src/FormDialog.vue +4 -0
- package/src/FormDrawer.vue +4 -0
- package/src/containers/Container.vue +178 -54
- package/src/containers/Fieldset.vue +5 -2
- package/src/containers/GroupList.vue +1 -1
- package/src/containers/GroupListItem.vue +4 -3
- package/src/containers/Tabs.vue +10 -2
- package/src/containers/table/Table.vue +9 -3
- package/src/schema.ts +47 -0
- package/src/submitForm.ts +196 -16
- package/src/theme/container.scss +4 -1
- package/types/index.d.ts +1021 -421
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
|
/**
|
|
@@ -45,9 +45,19 @@ export interface SubmitFormOptions {
|
|
|
45
45
|
keyProp?: string;
|
|
46
46
|
popperClass?: string;
|
|
47
47
|
preventSubmitDefault?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* 表单校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
|
|
50
|
+
* 默认 `true`,置为 `false` 时直接使用字段 name。
|
|
51
|
+
*/
|
|
52
|
+
useFieldTextInError?: boolean;
|
|
48
53
|
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
|
49
54
|
/** 透传给 Form.submitForm 的参数:是否直接返回原始响应式 values */
|
|
50
55
|
native?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* 是否在 resolve 结果中携带 changeRecords(变更记录)。
|
|
58
|
+
* 开启后 resolve 的结果为 `{ values, changeRecords }`,否则仅 resolve values。
|
|
59
|
+
*/
|
|
60
|
+
returnChangeRecords?: boolean;
|
|
51
61
|
/**
|
|
52
62
|
* 父级应用上下文,用于继承全局组件、指令、provide 等。
|
|
53
63
|
* 通常通过 `app._context` 或 `getCurrentInstance()?.appContext` 获取。
|
|
@@ -55,9 +65,30 @@ export interface SubmitFormOptions {
|
|
|
55
65
|
appContext?: AppContext | null;
|
|
56
66
|
/** 等待表单初始化的最长时间(毫秒),超时将以错误 reject。默认 10000ms */
|
|
57
67
|
timeout?: number;
|
|
68
|
+
/**
|
|
69
|
+
* 调试模式。默认 `false`。
|
|
70
|
+
*
|
|
71
|
+
* - `false`:表单以隐藏方式挂载,初始化完成后自动提交(原有行为)。
|
|
72
|
+
* - `true`:将表单以弹层形式可见地渲染在页面上,需手动点击「确定」才会触发校验/提交,
|
|
73
|
+
* 点击「取消」则以 reject 中断;校验失败时保留弹层并展示错误信息,便于修正后重试。
|
|
74
|
+
* 调试模式下 `timeout` 不生效(等待人工操作)。
|
|
75
|
+
*/
|
|
76
|
+
debug?: boolean;
|
|
58
77
|
}
|
|
59
78
|
// #endregion SubmitFormOptions
|
|
60
79
|
|
|
80
|
+
// #region SubmitFormResult
|
|
81
|
+
/**
|
|
82
|
+
* 开启 `returnChangeRecords` 时 submitForm 的返回结果
|
|
83
|
+
*/
|
|
84
|
+
export interface SubmitFormResult {
|
|
85
|
+
/** 校验通过后的表单值 */
|
|
86
|
+
values: any;
|
|
87
|
+
/** 表单变更记录 */
|
|
88
|
+
changeRecords: ChangeRecord[];
|
|
89
|
+
}
|
|
90
|
+
// #endregion SubmitFormResult
|
|
91
|
+
|
|
61
92
|
/**
|
|
62
93
|
* 以命令式方式调用 Form.vue 完成一次表单校验/提交。
|
|
63
94
|
*
|
|
@@ -78,14 +109,31 @@ export interface SubmitFormOptions {
|
|
|
78
109
|
* } catch (e) {
|
|
79
110
|
* console.error(e);
|
|
80
111
|
* }
|
|
112
|
+
*
|
|
113
|
+
* // 需要同时获取变更记录时:
|
|
114
|
+
* const { values, changeRecords } = await submitForm({
|
|
115
|
+
* config: [...],
|
|
116
|
+
* initValues: { name: 'foo' },
|
|
117
|
+
* returnChangeRecords: true,
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* // 调试模式:可见地渲染表单,点击「确定」才提交:
|
|
121
|
+
* const values = await submitForm({
|
|
122
|
+
* config: [...],
|
|
123
|
+
* initValues: { name: 'foo' },
|
|
124
|
+
* debug: true,
|
|
125
|
+
* });
|
|
81
126
|
* ```
|
|
82
127
|
*/
|
|
83
128
|
export const submitForm = (options: SubmitFormOptions): Promise<any> => {
|
|
84
|
-
const { native, appContext, timeout = 10000, ...formProps } = options;
|
|
129
|
+
const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, ...formProps } = options;
|
|
85
130
|
|
|
86
131
|
return new Promise((resolve, reject) => {
|
|
87
132
|
const container = document.createElement('div');
|
|
88
|
-
|
|
133
|
+
// 调试模式下需要把表单展示出来,普通模式则隐藏挂载
|
|
134
|
+
if (!debug) {
|
|
135
|
+
container.style.display = 'none';
|
|
136
|
+
}
|
|
89
137
|
document.body.appendChild(container);
|
|
90
138
|
|
|
91
139
|
let cleaned = false;
|
|
@@ -95,23 +143,154 @@ export const submitForm = (options: SubmitFormOptions): Promise<any> => {
|
|
|
95
143
|
name: 'MFormSubmitWrapper',
|
|
96
144
|
setup() {
|
|
97
145
|
const formRef = ref<any>(null);
|
|
146
|
+
// 调试模式下用于展示校验失败信息
|
|
147
|
+
const errorMsg = ref('');
|
|
148
|
+
|
|
149
|
+
const doSubmit = async () => {
|
|
150
|
+
try {
|
|
151
|
+
// 等待子组件(FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
|
|
152
|
+
await nextTick();
|
|
153
|
+
// submitForm 校验通过后会清空 changeRecords,需在调用前先做快照
|
|
154
|
+
const changeRecords: ChangeRecord[] = [...(formRef.value?.changeRecords ?? [])];
|
|
155
|
+
const result = await formRef.value.submitForm(native);
|
|
156
|
+
resolve(returnChangeRecords ? { values: result, changeRecords } : result);
|
|
157
|
+
cleanup();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// 调试模式下校验失败时保留弹层并展示错误,便于修正后重新提交
|
|
160
|
+
if (debug) {
|
|
161
|
+
errorMsg.value = err instanceof Error ? err.message : String(err);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
reject(err);
|
|
165
|
+
cleanup();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// 调试模式:可见地渲染表单,点击「确定」才提交,点击「取消」则中断
|
|
170
|
+
if (debug) {
|
|
171
|
+
const handleCancel = () => {
|
|
172
|
+
reject(new Error('submitForm canceled in debug mode.'));
|
|
173
|
+
cleanup();
|
|
174
|
+
};
|
|
98
175
|
|
|
176
|
+
const btnBase = {
|
|
177
|
+
padding: '8px 20px',
|
|
178
|
+
fontSize: '14px',
|
|
179
|
+
lineHeight: '1',
|
|
180
|
+
border: '1px solid #dcdfe6',
|
|
181
|
+
borderRadius: '4px',
|
|
182
|
+
cursor: 'pointer',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return () =>
|
|
186
|
+
h(
|
|
187
|
+
'div',
|
|
188
|
+
{
|
|
189
|
+
style: {
|
|
190
|
+
position: 'fixed',
|
|
191
|
+
inset: '0',
|
|
192
|
+
zIndex: '10000',
|
|
193
|
+
display: 'flex',
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
justifyContent: 'center',
|
|
196
|
+
background: 'rgba(0, 0, 0, 0.5)',
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
[
|
|
200
|
+
h(
|
|
201
|
+
'div',
|
|
202
|
+
{
|
|
203
|
+
style: {
|
|
204
|
+
display: 'flex',
|
|
205
|
+
flexDirection: 'column',
|
|
206
|
+
width: '600px',
|
|
207
|
+
maxWidth: '90vw',
|
|
208
|
+
maxHeight: '85vh',
|
|
209
|
+
background: '#fff',
|
|
210
|
+
borderRadius: '8px',
|
|
211
|
+
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.2)',
|
|
212
|
+
overflow: 'hidden',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
[
|
|
216
|
+
h(
|
|
217
|
+
'div',
|
|
218
|
+
{
|
|
219
|
+
style: {
|
|
220
|
+
padding: '16px 20px',
|
|
221
|
+
fontSize: '16px',
|
|
222
|
+
fontWeight: '600',
|
|
223
|
+
borderBottom: '1px solid #ebeef5',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
'submitForm 调试',
|
|
227
|
+
),
|
|
228
|
+
h(
|
|
229
|
+
'div',
|
|
230
|
+
{
|
|
231
|
+
style: {
|
|
232
|
+
flex: '1',
|
|
233
|
+
padding: '20px',
|
|
234
|
+
overflow: 'auto',
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
[
|
|
238
|
+
h(Form as Component, { ...formProps, ref: formRef }),
|
|
239
|
+
errorMsg.value
|
|
240
|
+
? h('div', {
|
|
241
|
+
style: {
|
|
242
|
+
marginTop: '12px',
|
|
243
|
+
color: '#f56c6c',
|
|
244
|
+
fontSize: '13px',
|
|
245
|
+
lineHeight: '1.5',
|
|
246
|
+
},
|
|
247
|
+
innerHTML: errorMsg.value,
|
|
248
|
+
})
|
|
249
|
+
: null,
|
|
250
|
+
],
|
|
251
|
+
),
|
|
252
|
+
h(
|
|
253
|
+
'div',
|
|
254
|
+
{
|
|
255
|
+
style: {
|
|
256
|
+
display: 'flex',
|
|
257
|
+
justifyContent: 'flex-end',
|
|
258
|
+
gap: '12px',
|
|
259
|
+
padding: '12px 20px',
|
|
260
|
+
borderTop: '1px solid #ebeef5',
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
[
|
|
264
|
+
h('button', { type: 'button', onClick: handleCancel, style: { ...btnBase } }, '取消'),
|
|
265
|
+
h(
|
|
266
|
+
'button',
|
|
267
|
+
{
|
|
268
|
+
type: 'button',
|
|
269
|
+
onClick: doSubmit,
|
|
270
|
+
style: {
|
|
271
|
+
...btnBase,
|
|
272
|
+
color: '#fff',
|
|
273
|
+
background: '#409eff',
|
|
274
|
+
borderColor: '#409eff',
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
'确定',
|
|
278
|
+
),
|
|
279
|
+
],
|
|
280
|
+
),
|
|
281
|
+
],
|
|
282
|
+
),
|
|
283
|
+
],
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 普通模式:表单初始化完成后自动提交
|
|
99
288
|
const stop = watch(
|
|
100
289
|
() => formRef.value?.initialized,
|
|
101
|
-
|
|
290
|
+
(initialized) => {
|
|
102
291
|
if (!initialized) return;
|
|
103
292
|
stop();
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
// 等待子组件(FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
|
|
107
|
-
await nextTick();
|
|
108
|
-
const result = await formRef.value.submitForm(native);
|
|
109
|
-
resolve(result);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
reject(err);
|
|
112
|
-
} finally {
|
|
113
|
-
cleanup();
|
|
114
|
-
}
|
|
293
|
+
doSubmit();
|
|
115
294
|
},
|
|
116
295
|
{ flush: 'post', immediate: true },
|
|
117
296
|
);
|
|
@@ -142,7 +321,8 @@ export const submitForm = (options: SubmitFormOptions): Promise<any> => {
|
|
|
142
321
|
container.parentNode?.removeChild(container);
|
|
143
322
|
};
|
|
144
323
|
|
|
145
|
-
|
|
324
|
+
// 调试模式等待人工操作,不应用超时
|
|
325
|
+
if (timeout > 0 && !debug) {
|
|
146
326
|
timer = setTimeout(() => {
|
|
147
327
|
if (!cleaned) {
|
|
148
328
|
reject(new Error(`submitForm timeout after ${timeout}ms: form is not initialized.`));
|