@tmagic/form 1.8.0-beta.2 → 1.8.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/Form.vue_vue_type_script_setup_true_lang.js +120 -27
- package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +19 -3
- package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +30 -7
- package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +56 -14
- package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +51 -36
- 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 +7 -6
- package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +42 -32
- package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +17 -11
- 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 +1 -1
- package/dist/es/containers/table/ActionsColumn.vue_vue_type_script_setup_true_lang.js +14 -7
- 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 +7 -5
- package/dist/es/containers/table/useTableColumns.js +5 -1
- package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/Checkbox.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/CheckboxGroup.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/ColorPicker.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/Date.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/DateTime.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/Daterange.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/Display.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/DynamicField.vue_vue_type_script_setup_true_lang.js +5 -2
- 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 +30 -8
- package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +8 -3
- package/dist/es/fields/NumberRange.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/RadioGroup.vue_vue_type_script_setup_true_lang.js +21 -10
- package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +7 -3
- package/dist/es/fields/Switch.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +113 -78
- package/dist/es/fields/Textarea.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/Time.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/Timerange.vue_vue_type_script_setup_true_lang.js +7 -4
- package/dist/es/index.js +5 -4
- package/dist/es/plugin.js +4 -0
- package/dist/es/schema.js +12 -1
- package/dist/es/style.css +133 -11
- package/dist/es/submitForm.js +415 -56
- package/dist/es/utils/config.js +4 -1
- package/dist/es/utils/form.js +84 -9
- package/dist/es/utils/typeMatch.js +487 -0
- package/dist/style.css +133 -11
- package/dist/themes/magic-admin.css +509 -0
- package/dist/tmagic-form.umd.cjs +1773 -399
- package/package.json +5 -5
- package/src/Form.vue +163 -30
- package/src/FormBox.vue +11 -0
- package/src/FormDialog.vue +25 -3
- package/src/FormDrawer.vue +51 -12
- package/src/containers/Col.vue +1 -0
- package/src/containers/Container.vue +32 -11
- package/src/containers/Fieldset.vue +5 -2
- package/src/containers/GroupList.vue +1 -1
- package/src/containers/GroupListItem.vue +28 -22
- package/src/containers/Panel.vue +10 -3
- package/src/containers/table/ActionsColumn.vue +10 -4
- package/src/containers/table/Table.vue +1 -0
- package/src/containers/table/useTableColumns.ts +7 -3
- package/src/containers/table-group-list/TableGroupList.vue +7 -1
- package/src/fields/Link.vue +16 -3
- package/src/fields/Number.vue +2 -1
- package/src/fields/RadioGroup.vue +24 -6
- package/src/fields/Text.vue +43 -19
- package/src/index.ts +11 -0
- package/src/plugin.ts +12 -0
- package/src/schema.ts +13 -1
- package/src/submitForm.ts +699 -66
- package/src/theme/container.scss +3 -0
- package/src/theme/fieldset.scss +51 -0
- package/src/theme/form-dialog.scss +1 -1
- package/src/theme/form.scss +6 -0
- package/src/theme/group-list.scss +19 -8
- package/src/theme/index.scss +1 -0
- package/src/theme/link.scss +17 -0
- package/src/theme/panel.scss +1 -1
- package/src/theme/radio-group.scss +23 -0
- package/src/theme/table.scss +11 -1
- package/src/theme/tabs.scss +2 -2
- package/src/theme/text.scss +38 -0
- package/src/theme/themes/magic-admin/index.scss +106 -0
- package/src/utils/config.ts +7 -1
- package/src/utils/form.ts +144 -22
- package/src/utils/typeMatch.ts +870 -0
- package/types/index.d.ts +408 -21
package/dist/es/submitForm.js
CHANGED
|
@@ -1,6 +1,200 @@
|
|
|
1
|
+
import { FORM_SILENT_MODE_KEY } from "./schema.js";
|
|
2
|
+
import { applyExtendState } from "./utils/form.js";
|
|
1
3
|
import Form_default from "./Form.js";
|
|
2
|
-
import { createApp, defineComponent, h, nextTick, ref, watch } from "vue";
|
|
4
|
+
import { createApp, defineComponent, h, nextTick, provide, ref, watch } from "vue";
|
|
3
5
|
//#region packages/form/src/submitForm.ts
|
|
6
|
+
/** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
|
|
7
|
+
var DEFAULT_MOUNT_TIMEOUT = 1e4;
|
|
8
|
+
/**
|
|
9
|
+
* submitForm / validateForm 的公共脚手架:
|
|
10
|
+
*
|
|
11
|
+
* 创建临时容器 → 挂载一个 wrapper 组件(内含 MForm)→ 提供统一的 cleanup / timeout / appContext 继承 →
|
|
12
|
+
* 由 `createWrapper` 决定「何时调用 MForm 的哪个方法、如何 resolve/reject」。
|
|
13
|
+
*
|
|
14
|
+
* 调用方只需关注差异逻辑(调用 `submitForm` 还是 `validate`、结果如何包装、是否需要调试弹层),
|
|
15
|
+
* 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
|
|
16
|
+
*/
|
|
17
|
+
var mountFormInstance = (options) => {
|
|
18
|
+
const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
if (signal?.aborted) {
|
|
21
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let cleaned = false;
|
|
25
|
+
let timer = null;
|
|
26
|
+
let onAbort = null;
|
|
27
|
+
const instance = { app: null };
|
|
28
|
+
const container = document.createElement("div");
|
|
29
|
+
if (hidden) container.style.display = "none";
|
|
30
|
+
document.body.appendChild(container);
|
|
31
|
+
const cleanup = () => {
|
|
32
|
+
if (cleaned) return;
|
|
33
|
+
cleaned = true;
|
|
34
|
+
if (timer) {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
timer = null;
|
|
37
|
+
}
|
|
38
|
+
if (signal && onAbort) {
|
|
39
|
+
signal.removeEventListener("abort", onAbort);
|
|
40
|
+
onAbort = null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
instance.app?.unmount();
|
|
44
|
+
} catch {}
|
|
45
|
+
container.parentNode?.removeChild(container);
|
|
46
|
+
};
|
|
47
|
+
if (signal) {
|
|
48
|
+
onAbort = () => {
|
|
49
|
+
if (cleaned) return;
|
|
50
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
|
|
51
|
+
cleanup();
|
|
52
|
+
};
|
|
53
|
+
signal.addEventListener("abort", onAbort);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const formRef = ref(null);
|
|
57
|
+
const { extendState, ...restFormProps } = formProps;
|
|
58
|
+
const userWrapper = createWrapper({
|
|
59
|
+
formRef,
|
|
60
|
+
formProps: restFormProps,
|
|
61
|
+
cleanup,
|
|
62
|
+
resolve,
|
|
63
|
+
reject
|
|
64
|
+
});
|
|
65
|
+
const wrapperComponent = typeof extendState === "function" ? defineComponent({
|
|
66
|
+
name: "MFormExtendStateInjector",
|
|
67
|
+
setup() {
|
|
68
|
+
watch(() => formRef.value, (form) => {
|
|
69
|
+
if (!form) return;
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = extendState(form.formState);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error("[MForm] extendState failed:", e);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
|
|
78
|
+
const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
|
|
79
|
+
if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
|
|
80
|
+
else apply(result);
|
|
81
|
+
}, {
|
|
82
|
+
flush: "sync",
|
|
83
|
+
immediate: true
|
|
84
|
+
});
|
|
85
|
+
return () => h(userWrapper);
|
|
86
|
+
}
|
|
87
|
+
}) : userWrapper;
|
|
88
|
+
const app = createApp(hidden ? defineComponent({
|
|
89
|
+
name: "MFormSilentProvider",
|
|
90
|
+
setup() {
|
|
91
|
+
provide(FORM_SILENT_MODE_KEY, true);
|
|
92
|
+
return () => h(wrapperComponent);
|
|
93
|
+
}
|
|
94
|
+
}) : wrapperComponent);
|
|
95
|
+
instance.app = app;
|
|
96
|
+
if (appContext) Object.assign(app._context, appContext);
|
|
97
|
+
if (!skipTimeout) timer = setTimeout(() => {
|
|
98
|
+
if (!cleaned) {
|
|
99
|
+
reject(new Error(timeoutMessage));
|
|
100
|
+
cleanup();
|
|
101
|
+
}
|
|
102
|
+
}, timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT);
|
|
103
|
+
app.mount(container);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
reject(err);
|
|
106
|
+
cleanup();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* 构造调试模式下的可见弹层 wrapper:以 fixed 遮罩居中渲染 MForm,提供「确定 / 取消」按钮与错误展示区。
|
|
112
|
+
*
|
|
113
|
+
* submitForm 与 validateForm 的调试弹层 UI 完全一致,仅「确定」时调用的实例方法、错误处理与取消文案不同,
|
|
114
|
+
* 这些差异通过 `onConfirm` / `onCancel` 注入,弹层结构在此统一收口。
|
|
115
|
+
*/
|
|
116
|
+
var createDebugWrapper = (options) => {
|
|
117
|
+
const { formRef, formProps, title, name, onConfirm, onCancel } = options;
|
|
118
|
+
const btnBase = {
|
|
119
|
+
padding: "8px 20px",
|
|
120
|
+
fontSize: "14px",
|
|
121
|
+
lineHeight: "1",
|
|
122
|
+
border: "1px solid #dcdfe6",
|
|
123
|
+
borderRadius: "4px",
|
|
124
|
+
cursor: "pointer"
|
|
125
|
+
};
|
|
126
|
+
return defineComponent({
|
|
127
|
+
name,
|
|
128
|
+
setup() {
|
|
129
|
+
const errorMsg = ref("");
|
|
130
|
+
const setError = (msg) => {
|
|
131
|
+
errorMsg.value = msg;
|
|
132
|
+
};
|
|
133
|
+
return () => h("div", { style: {
|
|
134
|
+
position: "fixed",
|
|
135
|
+
inset: "0",
|
|
136
|
+
zIndex: "10000",
|
|
137
|
+
display: "flex",
|
|
138
|
+
alignItems: "center",
|
|
139
|
+
justifyContent: "center",
|
|
140
|
+
background: "rgba(0, 0, 0, 0.5)"
|
|
141
|
+
} }, [h("div", { style: {
|
|
142
|
+
display: "flex",
|
|
143
|
+
flexDirection: "column",
|
|
144
|
+
width: "600px",
|
|
145
|
+
maxWidth: "90vw",
|
|
146
|
+
maxHeight: "85vh",
|
|
147
|
+
background: "#fff",
|
|
148
|
+
borderRadius: "8px",
|
|
149
|
+
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.2)",
|
|
150
|
+
overflow: "hidden"
|
|
151
|
+
} }, [
|
|
152
|
+
h("div", { style: {
|
|
153
|
+
padding: "16px 20px",
|
|
154
|
+
fontSize: "16px",
|
|
155
|
+
fontWeight: "600",
|
|
156
|
+
borderBottom: "1px solid #ebeef5"
|
|
157
|
+
} }, title),
|
|
158
|
+
h("div", { style: {
|
|
159
|
+
flex: "1",
|
|
160
|
+
padding: "20px",
|
|
161
|
+
overflow: "auto"
|
|
162
|
+
} }, [h(Form_default, {
|
|
163
|
+
...formProps,
|
|
164
|
+
ref: formRef
|
|
165
|
+
}), errorMsg.value ? h("div", {
|
|
166
|
+
style: {
|
|
167
|
+
marginTop: "12px",
|
|
168
|
+
color: "#f56c6c",
|
|
169
|
+
fontSize: "13px",
|
|
170
|
+
lineHeight: "1.5"
|
|
171
|
+
},
|
|
172
|
+
innerHTML: errorMsg.value
|
|
173
|
+
}) : null]),
|
|
174
|
+
h("div", { style: {
|
|
175
|
+
display: "flex",
|
|
176
|
+
justifyContent: "flex-end",
|
|
177
|
+
gap: "12px",
|
|
178
|
+
padding: "12px 20px",
|
|
179
|
+
borderTop: "1px solid #ebeef5"
|
|
180
|
+
} }, [h("button", {
|
|
181
|
+
type: "button",
|
|
182
|
+
onClick: onCancel,
|
|
183
|
+
style: { ...btnBase }
|
|
184
|
+
}, "取消"), h("button", {
|
|
185
|
+
type: "button",
|
|
186
|
+
onClick: () => onConfirm(setError),
|
|
187
|
+
style: {
|
|
188
|
+
...btnBase,
|
|
189
|
+
color: "#fff",
|
|
190
|
+
background: "#409eff",
|
|
191
|
+
borderColor: "#409eff"
|
|
192
|
+
}
|
|
193
|
+
}, "确定")])
|
|
194
|
+
])]);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
};
|
|
4
198
|
/**
|
|
5
199
|
* 以命令式方式调用 Form.vue 完成一次表单校验/提交。
|
|
6
200
|
*
|
|
@@ -21,67 +215,232 @@ import { createApp, defineComponent, h, nextTick, ref, watch } from "vue";
|
|
|
21
215
|
* } catch (e) {
|
|
22
216
|
* console.error(e);
|
|
23
217
|
* }
|
|
218
|
+
*
|
|
219
|
+
* // 需要同时获取变更记录时:
|
|
220
|
+
* const { values, changeRecords } = await submitForm({
|
|
221
|
+
* config: [...],
|
|
222
|
+
* initValues: { name: 'foo' },
|
|
223
|
+
* returnChangeRecords: true,
|
|
224
|
+
* });
|
|
225
|
+
*
|
|
226
|
+
* // 调试模式:可见地渲染表单,点击「确定」才提交:
|
|
227
|
+
* const values = await submitForm({
|
|
228
|
+
* config: [...],
|
|
229
|
+
* initValues: { name: 'foo' },
|
|
230
|
+
* debug: true,
|
|
231
|
+
* });
|
|
24
232
|
* ```
|
|
25
233
|
*/
|
|
26
234
|
var submitForm = (options) => {
|
|
27
|
-
const { native, appContext, timeout = 1e4, ...formProps } = options;
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
235
|
+
const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
|
|
236
|
+
return mountFormInstance({
|
|
237
|
+
formProps,
|
|
238
|
+
appContext,
|
|
239
|
+
timeout,
|
|
240
|
+
signal,
|
|
241
|
+
hidden: !debug,
|
|
242
|
+
skipTimeout: debug,
|
|
243
|
+
timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
|
|
244
|
+
createWrapper: ({ formRef, formProps, cleanup, resolve, reject }) => {
|
|
245
|
+
/**
|
|
246
|
+
* 执行一次提交:nextTick 等待子组件渲染 → 快照 changeRecords → 调用实例 submitForm → resolve。
|
|
247
|
+
* 校验失败时交给 `onValidateError` 决定「保留弹层展示错误(debug)」还是「reject 并清理(普通)」,
|
|
248
|
+
* 从而让 debug 与普通模式共享同一份提交逻辑。
|
|
249
|
+
*/
|
|
250
|
+
const doSubmit = async (onValidateError) => {
|
|
251
|
+
try {
|
|
252
|
+
await nextTick();
|
|
253
|
+
const changeRecords = [...formRef.value?.changeRecords ?? []];
|
|
254
|
+
const result = await formRef.value.submitForm(native);
|
|
255
|
+
resolve(returnChangeRecords ? {
|
|
256
|
+
values: result,
|
|
257
|
+
changeRecords
|
|
258
|
+
} : result);
|
|
259
|
+
cleanup();
|
|
260
|
+
} catch (err) {
|
|
261
|
+
onValidateError(err);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
if (debug) return createDebugWrapper({
|
|
265
|
+
formRef,
|
|
266
|
+
formProps,
|
|
267
|
+
name: "MFormSubmitWrapper",
|
|
268
|
+
title: "submitForm 调试",
|
|
269
|
+
onConfirm: (setError) => doSubmit((err) => {
|
|
270
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
271
|
+
}),
|
|
272
|
+
onCancel: () => {
|
|
273
|
+
reject(/* @__PURE__ */ new Error("submitForm canceled in debug mode."));
|
|
274
|
+
cleanup();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return defineComponent({
|
|
278
|
+
name: "MFormSubmitWrapper",
|
|
279
|
+
setup() {
|
|
280
|
+
const stop = watch(() => formRef.value?.initialized, (initialized) => {
|
|
281
|
+
if (!initialized) return;
|
|
282
|
+
stop();
|
|
283
|
+
doSubmit((err) => {
|
|
284
|
+
reject(err);
|
|
285
|
+
cleanup();
|
|
286
|
+
});
|
|
287
|
+
}, {
|
|
288
|
+
flush: "post",
|
|
289
|
+
immediate: true
|
|
290
|
+
});
|
|
291
|
+
return () => h(Form_default, {
|
|
292
|
+
...formProps,
|
|
293
|
+
ref: formRef
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
/**
|
|
301
|
+
* 深拷贝配置值,保留函数(display / onTabClick 等回调)引用,避免破坏配置中的回调。
|
|
302
|
+
*/
|
|
303
|
+
var cloneConfigValue = (value) => {
|
|
304
|
+
if (Array.isArray(value)) return value.map(cloneConfigValue);
|
|
305
|
+
if (value && typeof value === "object") return Object.keys(value).reduce((acc, key) => {
|
|
306
|
+
acc[key] = cloneConfigValue(value[key]);
|
|
307
|
+
return acc;
|
|
308
|
+
}, {});
|
|
309
|
+
return value;
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* 深度遍历配置,去掉所有 type 为 'tab' 的容器中各标签页(items)的 lazy 配置。
|
|
313
|
+
*/
|
|
314
|
+
var removeTabItemsLazy = (node) => {
|
|
315
|
+
if (Array.isArray(node)) {
|
|
316
|
+
node.forEach(removeTabItemsLazy);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (!node || typeof node !== "object") return;
|
|
320
|
+
if (node.type === "tab" && Array.isArray(node.items)) node.items.forEach((pane) => {
|
|
321
|
+
if (pane && typeof pane === "object") delete pane.lazy;
|
|
322
|
+
});
|
|
323
|
+
Object.keys(node).forEach((key) => {
|
|
324
|
+
removeTabItemsLazy(node[key]);
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* 返回一份去除了 tab 标签页 lazy 的配置副本。
|
|
329
|
+
*
|
|
330
|
+
* tab 容器开启 lazy 时,非激活标签页的内容不会渲染,导致 validateForm 静默挂载后
|
|
331
|
+
* 无法校验到这些标签页内的字段。校验场景需要一次性渲染全部字段,故在此统一去除 lazy。
|
|
332
|
+
* 处理基于深拷贝,不会污染调用方传入的原始 config。
|
|
333
|
+
*/
|
|
334
|
+
var stripTabItemsLazy = (config) => {
|
|
335
|
+
const cloned = cloneConfigValue(config);
|
|
336
|
+
removeTabItemsLazy(cloned);
|
|
337
|
+
return cloned;
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* 以命令式方式对一份「表单配置 + 值」做一次静默校验,**不依赖也不影响任何已渲染的表单**。
|
|
341
|
+
*
|
|
342
|
+
* 与 `submitForm` 类似,内部会临时挂载一个不可见的 MForm 实例,等待其初始化完成后调用
|
|
343
|
+
* 实例的 `validate` 方法,返回汇总后的错误文案,随后自动卸载实例。
|
|
344
|
+
*
|
|
345
|
+
* 与 `submitForm` 的区别:
|
|
346
|
+
* - 「静默」:校验失败不抛异常、不触发 `error` 事件、不返回表单值;
|
|
347
|
+
* - 仅用于「探测」配置是否合法,适合源码保存后校验、批量校验组件配置等场景。
|
|
348
|
+
*
|
|
349
|
+
* 由于每次都新建一个独立的 MForm 实例,调用方无需持有任何表单 ref,也不会污染
|
|
350
|
+
* 页面上正在展示的表单状态。
|
|
351
|
+
*
|
|
352
|
+
* @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
|
|
353
|
+
* 仅在初始化超时或挂载失败等异常情况下才会 reject。
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```ts
|
|
357
|
+
* import { validateForm } from '@tmagic/form';
|
|
358
|
+
*
|
|
359
|
+
* const error = await validateForm({
|
|
360
|
+
* config: [...],
|
|
361
|
+
* initValues: { name: 'foo' },
|
|
362
|
+
* appContext: getCurrentInstance()?.appContext,
|
|
363
|
+
* });
|
|
364
|
+
* if (error) {
|
|
365
|
+
* // 配置不合法,error 为错误文案
|
|
366
|
+
* }
|
|
367
|
+
*
|
|
368
|
+
* // 调试模式:可见地渲染表单,点击「确定」才校验,校验失败保留弹层可修正重试:
|
|
369
|
+
* const error = await validateForm({
|
|
370
|
+
* config: [...],
|
|
371
|
+
* initValues: { name: 'foo' },
|
|
372
|
+
* debug: true,
|
|
373
|
+
* });
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
var validateForm = (options) => {
|
|
377
|
+
const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
|
|
378
|
+
return mountFormInstance({
|
|
379
|
+
formProps: {
|
|
380
|
+
...rest,
|
|
381
|
+
config: stripTabItemsLazy(config)
|
|
382
|
+
},
|
|
383
|
+
appContext,
|
|
384
|
+
timeout,
|
|
385
|
+
signal,
|
|
386
|
+
hidden: !debug,
|
|
387
|
+
skipTimeout: debug,
|
|
388
|
+
timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
|
|
389
|
+
createWrapper: ({ formRef, formProps, cleanup, resolve, reject }) => {
|
|
390
|
+
/**
|
|
391
|
+
* 执行一次校验:nextTick 等待子组件渲染 → 调用实例 validate → 通过则 resolve ''。
|
|
392
|
+
* 校验失败(返回非空错误文案)时交给 `onInvalid` 决定「静默 resolve 错误文案(普通)」
|
|
393
|
+
* 还是「在弹层展示错误并保留供重试(debug)」,从而让两种模式共享同一份校验逻辑。
|
|
394
|
+
*/
|
|
395
|
+
const doValidate = async (onInvalid) => {
|
|
396
|
+
try {
|
|
397
|
+
await nextTick();
|
|
398
|
+
const error = await formRef.value.validate();
|
|
399
|
+
if (error) onInvalid(error);
|
|
400
|
+
else {
|
|
401
|
+
resolve("");
|
|
47
402
|
cleanup();
|
|
48
403
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
404
|
+
} catch (err) {
|
|
405
|
+
reject(err);
|
|
406
|
+
cleanup();
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
if (debug) return createDebugWrapper({
|
|
410
|
+
formRef,
|
|
411
|
+
formProps,
|
|
412
|
+
name: "MFormValidateWrapper",
|
|
413
|
+
title: "validateForm 调试",
|
|
414
|
+
onConfirm: (setError) => doValidate((error) => {
|
|
415
|
+
setError(error);
|
|
416
|
+
}),
|
|
417
|
+
onCancel: () => {
|
|
418
|
+
reject(/* @__PURE__ */ new Error("validateForm canceled in debug mode."));
|
|
419
|
+
cleanup();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
return defineComponent({
|
|
423
|
+
name: "MFormValidateWrapper",
|
|
424
|
+
setup() {
|
|
425
|
+
const stop = watch(() => formRef.value?.initialized, (initialized) => {
|
|
426
|
+
if (!initialized) return;
|
|
427
|
+
stop();
|
|
428
|
+
doValidate((error) => {
|
|
429
|
+
resolve(error);
|
|
430
|
+
cleanup();
|
|
431
|
+
});
|
|
432
|
+
}, {
|
|
433
|
+
flush: "post",
|
|
434
|
+
immediate: true
|
|
435
|
+
});
|
|
436
|
+
return () => h(Form_default, {
|
|
437
|
+
...formProps,
|
|
438
|
+
ref: formRef
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
});
|
|
83
442
|
}
|
|
84
443
|
});
|
|
85
444
|
};
|
|
86
445
|
//#endregion
|
|
87
|
-
export { submitForm };
|
|
446
|
+
export { stripTabItemsLazy, submitForm, validateForm };
|
package/dist/es/utils/config.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
1
2
|
//#region packages/form/src/utils/config.ts
|
|
2
3
|
var $MAGIC_FORM = {};
|
|
4
|
+
var isGlobalFlat = ref(false);
|
|
3
5
|
var setConfig = (option) => {
|
|
4
6
|
$MAGIC_FORM = option;
|
|
7
|
+
isGlobalFlat.value = option.flat ?? false;
|
|
5
8
|
};
|
|
6
9
|
var getConfig = (key) => $MAGIC_FORM[key];
|
|
7
10
|
var fieldRegistry = /* @__PURE__ */ new Map();
|
|
@@ -12,4 +15,4 @@ var registerField = (tagName, component) => {
|
|
|
12
15
|
var getField = (tagName) => fieldRegistry.get(tagName);
|
|
13
16
|
var deleteField = (tagName) => fieldRegistry.delete(tagName);
|
|
14
17
|
//#endregion
|
|
15
|
-
export { deleteField, getConfig, getField, registerField, setConfig };
|
|
18
|
+
export { deleteField, getConfig, getField, isGlobalFlat, registerField, setConfig };
|
package/dist/es/utils/form.js
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
|
+
import { createTypeMatchValidator } from "./typeMatch.js";
|
|
1
2
|
import { readonly } from "vue";
|
|
2
|
-
import { cloneDeep } from "lodash-es";
|
|
3
|
-
import { getValueByKeyPath } from "@tmagic/utils";
|
|
4
3
|
import dayjs from "dayjs";
|
|
5
4
|
import utc from "dayjs/plugin/utc";
|
|
5
|
+
import { cloneDeep } from "lodash-es";
|
|
6
|
+
import { getDesignConfig } from "@tmagic/design";
|
|
7
|
+
import { getValueByKeyPath } from "@tmagic/utils";
|
|
6
8
|
//#region packages/form/src/utils/form.ts
|
|
7
|
-
var
|
|
9
|
+
var isTDesignAdapter = () => getDesignConfig("adapterType") === "tdesign-vue-next";
|
|
10
|
+
/**
|
|
11
|
+
* 将 async-validator(Element Plus)风格的 validator 适配到当前 UI 库。
|
|
12
|
+
* TDesign 调用签名为 `(val) => boolean | CustomValidateObj | Promise`,无 callback。
|
|
13
|
+
*/
|
|
14
|
+
var adaptFormValidator = (validator) => {
|
|
15
|
+
return (arg1, arg2, arg3, arg4, arg5) => {
|
|
16
|
+
if (!isTDesignAdapter()) return validator(arg1, arg2, arg3, arg4, arg5);
|
|
17
|
+
const value = arg1;
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
let settled = false;
|
|
20
|
+
const callback = (error) => {
|
|
21
|
+
if (settled) return;
|
|
22
|
+
settled = true;
|
|
23
|
+
const first = Array.isArray(error) ? error[0] : error;
|
|
24
|
+
if (first) resolve({
|
|
25
|
+
result: false,
|
|
26
|
+
message: typeof first === "string" ? first : first.message
|
|
27
|
+
});
|
|
28
|
+
else resolve(true);
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
const result = validator(void 0, value, callback);
|
|
32
|
+
if (result && typeof result.then === "function") Promise.resolve(result).then(() => {
|
|
33
|
+
if (!settled) callback();
|
|
34
|
+
}, (err) => {
|
|
35
|
+
callback(err instanceof Error ? err : new Error(String(err)));
|
|
36
|
+
});
|
|
37
|
+
} catch (e) {
|
|
38
|
+
callback(e instanceof Error ? e : new Error(String(e)));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
var TABLE_SELECT_TYPES = /* @__PURE__ */ new Set(["table-select", "tableSelect"]);
|
|
8
44
|
var isTableSelect = (type) => typeof type === "string" && TABLE_SELECT_TYPES.has(type);
|
|
9
45
|
var asyncLoadConfig = (value, initValue, { asyncLoad, name, type }) => {
|
|
10
46
|
if (type === "html" && typeof asyncLoad === "object" && typeof name !== "undefined") {
|
|
@@ -12,7 +48,7 @@ var asyncLoadConfig = (value, initValue, { asyncLoad, name, type }) => {
|
|
|
12
48
|
value.asyncLoad = typeof initValue.asyncLoad === "object" ? initValue.asyncLoad : asyncLoad;
|
|
13
49
|
}
|
|
14
50
|
};
|
|
15
|
-
var MULTIPLE_VALUE_TYPES = new Set([
|
|
51
|
+
var MULTIPLE_VALUE_TYPES = /* @__PURE__ */ new Set([
|
|
16
52
|
"checkbox-group",
|
|
17
53
|
"checkboxGroup",
|
|
18
54
|
"table",
|
|
@@ -99,13 +135,18 @@ var display = function(mForm, config, props) {
|
|
|
99
135
|
if (config === false) return false;
|
|
100
136
|
return true;
|
|
101
137
|
};
|
|
102
|
-
var getRules = function(mForm,
|
|
103
|
-
rules = cloneDeep(
|
|
138
|
+
var getRules = function(mForm, r = [], props, typeMatchValid) {
|
|
139
|
+
let rules = cloneDeep(r);
|
|
104
140
|
if (typeof rules === "object" && !Array.isArray(rules)) rules = [rules];
|
|
141
|
+
if (typeMatchValid?.value && !rules.some((r) => typeof r.typeMatch !== "undefined")) rules.push({ typeMatch: true });
|
|
105
142
|
return rules.map((item) => {
|
|
143
|
+
if (item.typeMatch) {
|
|
144
|
+
item.validator = adaptFormValidator(createTypeMatchValidator(mForm, props, item));
|
|
145
|
+
return item;
|
|
146
|
+
}
|
|
106
147
|
if (typeof item.validator === "function") {
|
|
107
148
|
const fnc = item.validator;
|
|
108
|
-
item.validator = (rule, value, callback, source, options) => fnc({
|
|
149
|
+
item.validator = adaptFormValidator((rule, value, callback, source, options) => fnc({
|
|
109
150
|
rule,
|
|
110
151
|
value: props.config.names ? props.model : value,
|
|
111
152
|
callback,
|
|
@@ -118,7 +159,7 @@ var getRules = function(mForm, rules = [], props) {
|
|
|
118
159
|
formValue: mForm?.values || props.model,
|
|
119
160
|
prop: props.prop,
|
|
120
161
|
config: props.config
|
|
121
|
-
}, mForm);
|
|
162
|
+
}, mForm));
|
|
122
163
|
}
|
|
123
164
|
return item;
|
|
124
165
|
});
|
|
@@ -163,6 +204,40 @@ var sortChange = (data, { prop, order }) => {
|
|
|
163
204
|
if (order === "ascending") data.sort((a, b) => a[prop] - b[prop]);
|
|
164
205
|
else if (order === "descending") data.sort((a, b) => b[prop] - a[prop]);
|
|
165
206
|
};
|
|
207
|
+
/**
|
|
208
|
+
* 将 extendState 返回的扩展字段合并进 formState。
|
|
209
|
+
*
|
|
210
|
+
* - data 描述符(普通字段)通过 `formState[key] = value` 写入,走 reactive proxy 的 set,
|
|
211
|
+
* 触发依赖通知;
|
|
212
|
+
* - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
|
|
213
|
+
* 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
|
|
214
|
+
*
|
|
215
|
+
* 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
|
|
216
|
+
* 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
|
|
217
|
+
* config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
|
|
218
|
+
* post 等),命中这些 key 时统一跳过并告警。
|
|
219
|
+
*
|
|
220
|
+
* 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
|
|
221
|
+
* 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
|
|
222
|
+
* `TypeError: 'set' on proxy: trap returned falsish`。
|
|
223
|
+
*/
|
|
224
|
+
var applyExtendState = (formState, state, reservedKeys) => {
|
|
225
|
+
if (!state) return;
|
|
226
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
|
|
227
|
+
if (reservedKeys?.has(key)) continue;
|
|
228
|
+
if (!("value" in descriptor)) {
|
|
229
|
+
descriptor.configurable = true;
|
|
230
|
+
Object.defineProperty(formState, key, descriptor);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const targetDescriptor = Object.getOwnPropertyDescriptor(formState, key);
|
|
234
|
+
if (targetDescriptor && !("value" in targetDescriptor) && typeof targetDescriptor.set !== "function") {
|
|
235
|
+
console.warn(`[MForm] extendState: "${key}" is a read-only field derived from props and cannot be assigned a plain value. Return it as a getter accessor if you really need to override it.`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
formState[key] = state[key];
|
|
239
|
+
}
|
|
240
|
+
};
|
|
166
241
|
var createObjectProp = (prop, key, name) => {
|
|
167
242
|
if (prop === "") return key;
|
|
168
243
|
const itemPath = `${prop}`.split(".");
|
|
@@ -172,4 +247,4 @@ var createObjectProp = (prop, key, name) => {
|
|
|
172
247
|
return `${[...itemPath, key].join(".")}`;
|
|
173
248
|
};
|
|
174
249
|
//#endregion
|
|
175
|
-
export { createObjectProp, createValues, datetimeFormatter, display, filterFunction, getDataByPage, getRules, initValue, sortArray, sortChange };
|
|
250
|
+
export { adaptFormValidator, applyExtendState, createObjectProp, createValues, datetimeFormatter, display, filterFunction, getDataByPage, getRules, initValue, sortArray, sortChange };
|