@tmagic/form 1.8.0-beta.25 → 1.8.0-beta.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/Form.vue_vue_type_script_setup_true_lang.js +47 -120
- package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +3 -0
- package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +36 -51
- package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +6 -0
- package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/containers/GroupList.vue_vue_type_script_setup_true_lang.js +37 -15
- package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +109 -97
- package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +7 -2
- package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +3 -0
- package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +7 -0
- package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +7 -5
- package/dist/es/containers/table/useTableColumns.js +5 -13
- package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +71 -78
- package/dist/es/containers/table-group-list/useAdd.js +26 -16
- package/dist/es/containers/table-group-list/useScrollLastItemIntoView.js +72 -0
- package/dist/es/fields/Cascader.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/CheckboxGroup/Index.js +5 -0
- 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
- package/dist/es/fields/CheckboxGroup/effect.js +8 -0
- package/dist/es/fields/Date/Index.js +5 -0
- 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
- package/dist/es/fields/Date/effect.js +10 -0
- package/dist/es/fields/DateTime/Index.js +5 -0
- 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
- package/dist/es/fields/DateTime/effect.js +15 -0
- package/dist/es/fields/Display/Index.js +5 -0
- 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
- package/dist/es/fields/Display/effect.js +8 -0
- package/dist/es/fields/DynamicField/Index.js +5 -0
- 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
- package/dist/es/fields/DynamicField/effect.js +40 -0
- 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/Index.js +5 -0
- 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
- package/dist/es/fields/NumberRange/effect.js +8 -0
- package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +13 -6
- package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/headless.js +13 -0
- package/dist/es/index.js +17 -12
- package/dist/es/plugin.js +57 -41
- package/dist/es/schema.js +6 -8
- package/dist/es/style.css +81 -1
- package/dist/es/submitForm.js +44 -194
- package/dist/es/utils/builtInFields.js +49 -0
- package/dist/es/utils/collectFields.js +407 -0
- package/dist/es/utils/config.js +1 -8
- package/dist/es/utils/fieldInnerConfig.js +42 -0
- package/dist/es/utils/fieldValueEffects.js +80 -0
- package/dist/es/utils/form.js +96 -43
- package/dist/es/utils/formStateProxy.js +111 -0
- package/dist/es/utils/registerField.js +158 -0
- package/dist/es/utils/submitHeadless.js +54 -0
- package/dist/es/utils/tableGroupList.js +85 -0
- package/dist/es/utils/typeMatch.js +17 -19
- package/dist/es/utils/validateError.js +57 -0
- package/dist/es/utils/validateValues.js +115 -0
- package/dist/style.css +81 -1
- package/dist/themes/magic-admin.css +83 -7
- package/dist/tmagic-form-headless.umd.cjs +4080 -0
- package/dist/tmagic-form.umd.cjs +2020 -876
- package/package.json +10 -4
- package/src/Form.vue +49 -149
- package/src/FormBox.vue +5 -4
- package/src/FormDialog.vue +5 -5
- package/src/FormDrawer.vue +5 -5
- package/src/containers/Col.vue +2 -0
- package/src/containers/Container.vue +86 -157
- package/src/containers/Fieldset.vue +5 -0
- package/src/containers/FlexLayout.vue +2 -0
- package/src/containers/GroupList.vue +41 -11
- package/src/containers/GroupListItem.vue +83 -80
- package/src/containers/Panel.vue +3 -0
- package/src/containers/Row.vue +2 -0
- package/src/containers/Step.vue +2 -0
- package/src/containers/Tabs.vue +13 -0
- package/src/containers/table/ActionsColumn.vue +1 -1
- package/src/containers/table/Table.vue +4 -3
- package/src/containers/table/usePagination.ts +1 -1
- package/src/containers/table/useSortable.ts +1 -1
- package/src/containers/table/useTableColumns.ts +9 -19
- package/src/containers/table-group-list/TableGroupList.vue +43 -71
- package/src/containers/table-group-list/useAdd.ts +40 -25
- package/src/containers/table-group-list/useScrollLastItemIntoView.ts +94 -0
- package/src/fields/{CheckboxGroup.vue → CheckboxGroup/Index.vue} +3 -8
- package/src/fields/CheckboxGroup/effect.ts +27 -0
- package/src/fields/{Date.vue → Date/Index.vue} +2 -5
- package/src/fields/Date/effect.ts +29 -0
- package/src/fields/{DateTime.vue → DateTime/Index.vue} +2 -16
- package/src/fields/DateTime/effect.ts +36 -0
- package/src/fields/{Display.vue → Display/Index.vue} +3 -7
- package/src/fields/Display/effect.ts +28 -0
- package/src/fields/{DynamicField.vue → DynamicField/Index.vue} +11 -11
- package/src/fields/DynamicField/effect.ts +74 -0
- package/src/fields/{NumberRange.vue → NumberRange/Index.vue} +2 -6
- package/src/fields/NumberRange/effect.ts +27 -0
- package/src/fields/Select.vue +13 -4
- package/src/headless.ts +78 -0
- package/src/index.ts +32 -15
- package/src/plugin.ts +63 -42
- package/src/schema.ts +6 -8
- package/src/submitForm.ts +81 -409
- package/src/theme/group-list.scss +107 -1
- package/src/theme/themes/magic-admin/index.scss +2 -8
- package/src/utils/builtInFields.ts +69 -0
- package/src/utils/collectFields.ts +561 -0
- package/src/utils/config.ts +1 -15
- package/src/utils/fieldInnerConfig.ts +127 -0
- package/src/utils/fieldValueEffects.ts +140 -0
- package/src/utils/form.ts +141 -75
- package/src/utils/formStateProxy.ts +149 -0
- package/src/utils/registerField.ts +308 -0
- package/src/utils/submitHeadless.ts +206 -0
- package/src/utils/tableGroupList.ts +120 -0
- package/src/utils/typeMatch.ts +19 -15
- package/src/utils/validateError.ts +92 -0
- package/src/utils/validateValues.ts +177 -0
- package/types/headless.d.ts +682 -0
- package/types/index.d.ts +631 -272
- package/dist/es/fields/CheckboxGroup.js +0 -5
- package/dist/es/fields/Date.js +0 -5
- package/dist/es/fields/DateTime.js +0 -5
- package/dist/es/fields/Display.js +0 -5
- package/dist/es/fields/DynamicField.js +0 -5
- package/dist/es/fields/NumberRange.js +0 -5
- package/dist/es/utils/silentLeafFieldTypes.js +0 -49
- package/src/utils/silentLeafFieldTypes.ts +0 -71
package/src/submitForm.ts
CHANGED
|
@@ -16,102 +16,25 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { type AppContext, type Component, createApp, defineComponent, h, nextTick, type Ref, ref } from 'vue';
|
|
20
|
+
|
|
19
21
|
import {
|
|
20
|
-
|
|
21
|
-
type
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
provide,
|
|
27
|
-
type Ref,
|
|
28
|
-
ref,
|
|
29
|
-
watch,
|
|
30
|
-
} from 'vue';
|
|
31
|
-
|
|
32
|
-
import { applyExtendState } from './utils/form';
|
|
22
|
+
submitForm as submitFormHeadless,
|
|
23
|
+
type SubmitFormOptions,
|
|
24
|
+
type SubmitFormResult,
|
|
25
|
+
validateForm as validateFormHeadless,
|
|
26
|
+
type ValidateFormOptions,
|
|
27
|
+
} from './utils/submitHeadless';
|
|
33
28
|
import Form from './Form.vue';
|
|
34
|
-
import { type ChangeRecord,
|
|
29
|
+
import { type ChangeRecord, type FormConfig } from './schema';
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* submitForm 函数参数(与 Form.vue 组件 props 对齐)
|
|
39
|
-
*/
|
|
40
|
-
export interface SubmitFormOptions {
|
|
41
|
-
/** 表单配置 */
|
|
42
|
-
config: FormConfig;
|
|
43
|
-
/** 表单初始值 */
|
|
44
|
-
initValues?: Record<string, any>;
|
|
45
|
-
/** 需对比的值(开启对比模式时传入) */
|
|
46
|
-
lastValues?: Record<string, any>;
|
|
47
|
-
/** 是否开启对比模式 */
|
|
48
|
-
isCompare?: boolean;
|
|
49
|
-
parentValues?: Record<string, any>;
|
|
50
|
-
labelWidth?: string;
|
|
51
|
-
disabled?: boolean;
|
|
52
|
-
height?: string;
|
|
53
|
-
stepActive?: string | number;
|
|
54
|
-
size?: 'small' | 'default' | 'large';
|
|
55
|
-
inline?: boolean;
|
|
56
|
-
labelPosition?: string;
|
|
57
|
-
keyProp?: string;
|
|
58
|
-
popperClass?: string;
|
|
59
|
-
preventSubmitDefault?: boolean;
|
|
60
|
-
/**
|
|
61
|
-
* 表单校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
|
|
62
|
-
* 默认 `true`,置为 `false` 时直接使用字段 name。
|
|
63
|
-
*/
|
|
64
|
-
useFieldTextInError?: boolean;
|
|
65
|
-
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
|
66
|
-
/** 透传给 Form.submitForm 的参数:是否直接返回原始响应式 values */
|
|
67
|
-
native?: boolean;
|
|
68
|
-
/**
|
|
69
|
-
* 是否在 resolve 结果中携带 changeRecords(变更记录)。
|
|
70
|
-
* 开启后 resolve 的结果为 `{ values, changeRecords }`,否则仅 resolve values。
|
|
71
|
-
*/
|
|
72
|
-
returnChangeRecords?: boolean;
|
|
73
|
-
/**
|
|
74
|
-
* 父级应用上下文,用于继承全局组件、指令、provide 等。
|
|
75
|
-
* 通常通过 `app._context` 或 `getCurrentInstance()?.appContext` 获取。
|
|
76
|
-
*/
|
|
77
|
-
appContext?: AppContext | null;
|
|
78
|
-
/** 等待表单初始化的最长时间(毫秒),超时将以错误 reject。默认 10000ms */
|
|
79
|
-
timeout?: number;
|
|
80
|
-
/**
|
|
81
|
-
* 调试模式。默认 `false`。
|
|
82
|
-
*
|
|
83
|
-
* - `false`:表单以隐藏方式挂载,初始化完成后自动提交(原有行为)。
|
|
84
|
-
* - `true`:将表单以弹层形式可见地渲染在页面上,需手动点击「确定」才会触发校验/提交,
|
|
85
|
-
* 点击「取消」则以 reject 中断;校验失败时保留弹层并展示错误信息,便于修正后重试。
|
|
86
|
-
* 调试模式下 `timeout` 不生效(等待人工操作)。
|
|
87
|
-
*/
|
|
88
|
-
debug?: boolean;
|
|
89
|
-
typeMatchValid?: boolean;
|
|
90
|
-
/**
|
|
91
|
-
* 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
|
|
92
|
-
* 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
|
|
93
|
-
*/
|
|
94
|
-
signal?: AbortSignal;
|
|
95
|
-
}
|
|
96
|
-
// #endregion SubmitFormOptions
|
|
97
|
-
|
|
98
|
-
// #region SubmitFormResult
|
|
99
|
-
/**
|
|
100
|
-
* 开启 `returnChangeNodes` 时 submitForm 的返回结果
|
|
101
|
-
*/
|
|
102
|
-
export interface SubmitFormResult {
|
|
103
|
-
/** 校验通过后的表单值 */
|
|
104
|
-
values: any;
|
|
105
|
-
/** 表单变更记录 */
|
|
106
|
-
changeRecords: ChangeRecord[];
|
|
107
|
-
}
|
|
108
|
-
// #endregion SubmitFormResult
|
|
31
|
+
export type { SubmitFormOptions, SubmitFormResult, ValidateFormOptions };
|
|
109
32
|
|
|
110
33
|
// #region mountFormInstance
|
|
111
34
|
/**
|
|
112
35
|
* 构造 wrapper 组件的工厂:在合适时机调用 MForm 实例方法并 resolve/reject、清理实例。
|
|
113
36
|
*
|
|
114
|
-
* 由调用方决定「等待 `initialized`
|
|
37
|
+
* 由调用方决定「等待 `initialized` 后自动执行」还是「等待人工触发」(`dialog: true`),
|
|
115
38
|
* 以及「调用 `submitForm` 还是 `validate`」「结果如何包装」,从而复用公共脚手架。
|
|
116
39
|
*/
|
|
117
40
|
type FormWrapperFactory<T> = (ctx: {
|
|
@@ -132,43 +55,22 @@ interface MountFormInstanceOptions<T> {
|
|
|
132
55
|
formProps: Record<string, any>;
|
|
133
56
|
/** 父级应用上下文,用于继承全局组件、指令、provide 等 */
|
|
134
57
|
appContext?: AppContext | null;
|
|
135
|
-
/**
|
|
136
|
-
timeout: number;
|
|
137
|
-
/** 超时 reject 的错误文案 */
|
|
138
|
-
timeoutMessage: string;
|
|
139
|
-
/** 是否以 `display:none` 隐藏容器。调试模式需可见,应传 `false` */
|
|
140
|
-
hidden?: boolean;
|
|
141
|
-
/** 是否跳过超时注册。调试模式等待人工操作,应传 `true` */
|
|
142
|
-
skipTimeout?: boolean;
|
|
143
|
-
/** 外部中断信号:abort 时会 reject 并卸载实例、移除容器,用于取消无超时(如 debug)的挂载 */
|
|
58
|
+
/** 外部中断信号:abort 时会 reject 并卸载实例、移除容器,用于取消等待人工操作的挂载 */
|
|
144
59
|
signal?: AbortSignal;
|
|
145
60
|
/** 构造 wrapper 组件 */
|
|
146
61
|
createWrapper: FormWrapperFactory<T>;
|
|
147
62
|
}
|
|
148
63
|
|
|
149
|
-
/** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
|
|
150
|
-
const DEFAULT_MOUNT_TIMEOUT = 10000;
|
|
151
|
-
|
|
152
64
|
/**
|
|
153
|
-
* submitForm / validateForm 的公共脚手架:
|
|
65
|
+
* `dialog: true` 时 submitForm / validateForm 的公共脚手架:
|
|
154
66
|
*
|
|
155
|
-
* 创建临时容器 → 挂载一个 wrapper 组件(内含 MForm)→ 提供统一的 cleanup /
|
|
67
|
+
* 创建临时容器 → 挂载一个 wrapper 组件(内含 MForm)→ 提供统一的 cleanup / appContext 继承 →
|
|
156
68
|
* 由 `createWrapper` 决定「何时调用 MForm 的哪个方法、如何 resolve/reject」。
|
|
157
69
|
*
|
|
158
|
-
*
|
|
159
|
-
* 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
|
|
70
|
+
* 挂载出来的表单等待人工点击,因此没有超时兜底,靠「确定」/「取消」/`signal` 结束。
|
|
160
71
|
*/
|
|
161
72
|
const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T> => {
|
|
162
|
-
const {
|
|
163
|
-
formProps,
|
|
164
|
-
appContext,
|
|
165
|
-
timeout,
|
|
166
|
-
timeoutMessage,
|
|
167
|
-
hidden = true,
|
|
168
|
-
skipTimeout = false,
|
|
169
|
-
signal,
|
|
170
|
-
createWrapper,
|
|
171
|
-
} = options;
|
|
73
|
+
const { formProps, appContext, signal, createWrapper } = options;
|
|
172
74
|
|
|
173
75
|
return new Promise<T>((resolve, reject) => {
|
|
174
76
|
// 已中断则直接 reject,不创建任何容器/实例
|
|
@@ -178,24 +80,16 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
|
|
|
178
80
|
}
|
|
179
81
|
|
|
180
82
|
let cleaned = false;
|
|
181
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
182
83
|
let onAbort: (() => void) | null = null;
|
|
183
84
|
// 用 holder 持有 app,使 cleanup 可在 app 创建之前定义(const app + 无 TDZ / 无 use-before-define)
|
|
184
85
|
const instance: { app: ReturnType<typeof createApp> | null } = { app: null };
|
|
185
86
|
|
|
186
87
|
const container = document.createElement('div');
|
|
187
|
-
if (hidden) {
|
|
188
|
-
container.style.display = 'none';
|
|
189
|
-
}
|
|
190
88
|
document.body.appendChild(container);
|
|
191
89
|
|
|
192
90
|
const cleanup = () => {
|
|
193
91
|
if (cleaned) return;
|
|
194
92
|
cleaned = true;
|
|
195
|
-
if (timer) {
|
|
196
|
-
clearTimeout(timer);
|
|
197
|
-
timer = null;
|
|
198
|
-
}
|
|
199
93
|
if (signal && onAbort) {
|
|
200
94
|
signal.removeEventListener('abort', onAbort);
|
|
201
95
|
onAbort = null;
|
|
@@ -208,7 +102,7 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
|
|
|
208
102
|
container.parentNode?.removeChild(container);
|
|
209
103
|
};
|
|
210
104
|
|
|
211
|
-
// 支持外部通过 AbortSignal
|
|
105
|
+
// 支持外部通过 AbortSignal 主动中断:弹层无超时兜底,若调用方放弃了该 Promise,
|
|
212
106
|
// 可通过 abort 卸载实例、移除遮罩/容器,避免无限驻留在 DOM 中。
|
|
213
107
|
if (signal) {
|
|
214
108
|
onAbort = () => {
|
|
@@ -224,66 +118,9 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
|
|
|
224
118
|
try {
|
|
225
119
|
const formRef = ref<any>(null);
|
|
226
120
|
|
|
227
|
-
|
|
228
|
-
// 而是在 wrapper 中通过 sync watch 在 formRef 就绪后直接写入 formState,
|
|
229
|
-
// 避免 display 等 filterFunction 在首次渲染时读到 undefined。
|
|
230
|
-
// 与 CompareForm / FormPanel 中「formRef.value.formState.services = ...」的做法一致。
|
|
231
|
-
const { extendState, ...restFormProps } = formProps;
|
|
232
|
-
|
|
233
|
-
const userWrapper = createWrapper({ formRef, formProps: restFormProps, cleanup, resolve, reject });
|
|
234
|
-
|
|
235
|
-
const wrapperComponent =
|
|
236
|
-
typeof extendState === 'function'
|
|
237
|
-
? defineComponent({
|
|
238
|
-
name: 'MFormExtendStateInjector',
|
|
239
|
-
setup() {
|
|
240
|
-
watch(
|
|
241
|
-
() => formRef.value,
|
|
242
|
-
(form) => {
|
|
243
|
-
if (!form) return;
|
|
244
|
-
let result: any;
|
|
245
|
-
try {
|
|
246
|
-
result = extendState(form.formState);
|
|
247
|
-
} catch (e) {
|
|
248
|
-
console.error('[MForm] extendState failed:', e);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
// formState 的内置 key 快照:在 extendState 合并前捕获,
|
|
252
|
-
// 供 applyExtendState 禁止 extendState 覆盖这些已有字段(只能新增),
|
|
253
|
-
// 与 Form.vue 中 reservedStateKeys 的语义保持一致。
|
|
254
|
-
const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(form.formState));
|
|
255
|
-
// 合并逻辑收口在 applyExtendState:props 派生的只读 getter 字段
|
|
256
|
-
// (keyProp 等)以普通字段形式返回时会被跳过并告警,避免 proxy set 抛错
|
|
257
|
-
const apply = (state: Record<string, any> | null | undefined) =>
|
|
258
|
-
applyExtendState(form.formState, state, reservedStateKeys);
|
|
259
|
-
if (result && typeof result.then === 'function') {
|
|
260
|
-
result.then(apply, (e: any) => console.error('[MForm] extendState failed:', e));
|
|
261
|
-
} else {
|
|
262
|
-
apply(result);
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
{ flush: 'sync', immediate: true },
|
|
266
|
-
);
|
|
267
|
-
return () => h(userWrapper);
|
|
268
|
-
},
|
|
269
|
-
})
|
|
270
|
-
: userWrapper;
|
|
271
|
-
|
|
272
|
-
// 静默(隐藏挂载)模式下注入静默标记:vs-code 等重型字段组件可据此跳过自身渲染,
|
|
273
|
-
// 校验/取值依赖 FormItem 与 model 值,与叶子 UI 组件无关(见 FORM_SILENT_MODE_KEY 注释)。
|
|
274
|
-
// 用组件级 provide 而非 app.provide:appContext 合并后 app._context.provides 与父级应用
|
|
275
|
-
// 共享引用,app.provide 会把标记泄漏到父级应用。
|
|
276
|
-
const rootComponent = hidden
|
|
277
|
-
? defineComponent({
|
|
278
|
-
name: 'MFormSilentProvider',
|
|
279
|
-
setup() {
|
|
280
|
-
provide(FORM_SILENT_MODE_KEY, true);
|
|
281
|
-
return () => h(wrapperComponent);
|
|
282
|
-
},
|
|
283
|
-
})
|
|
284
|
-
: wrapperComponent;
|
|
121
|
+
const wrapperComponent = createWrapper({ formRef, formProps, cleanup, resolve, reject });
|
|
285
122
|
|
|
286
|
-
const app = createApp(
|
|
123
|
+
const app = createApp(wrapperComponent);
|
|
287
124
|
instance.app = app;
|
|
288
125
|
|
|
289
126
|
// 继承父级应用上下文(components / directives / provides / config 等)
|
|
@@ -291,18 +128,6 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
|
|
|
291
128
|
Object.assign(app._context, appContext);
|
|
292
129
|
}
|
|
293
130
|
|
|
294
|
-
// 非 debug(未跳过超时)场景始终注册超时兜底:timeout 为非正数时回退到默认值,
|
|
295
|
-
// 避免表单永不初始化时实例/容器/watcher 无限驻留而泄漏。
|
|
296
|
-
if (!skipTimeout) {
|
|
297
|
-
const effectiveTimeout = timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT;
|
|
298
|
-
timer = setTimeout(() => {
|
|
299
|
-
if (!cleaned) {
|
|
300
|
-
reject(new Error(timeoutMessage));
|
|
301
|
-
cleanup();
|
|
302
|
-
}
|
|
303
|
-
}, effectiveTimeout);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
131
|
app.mount(container);
|
|
307
132
|
} catch (err) {
|
|
308
133
|
reject(err);
|
|
@@ -312,8 +137,8 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
|
|
|
312
137
|
};
|
|
313
138
|
// #endregion mountFormInstance
|
|
314
139
|
|
|
315
|
-
// #region
|
|
316
|
-
interface
|
|
140
|
+
// #region createDialogWrapper
|
|
141
|
+
interface DialogWrapperOptions {
|
|
317
142
|
/** 指向挂载的 MForm 实例 */
|
|
318
143
|
formRef: Ref<any>;
|
|
319
144
|
/** 透传给 Form 组件的 props */
|
|
@@ -332,12 +157,12 @@ interface DebugWrapperOptions {
|
|
|
332
157
|
}
|
|
333
158
|
|
|
334
159
|
/**
|
|
335
|
-
*
|
|
160
|
+
* 构造可见弹层 wrapper:以 fixed 遮罩居中渲染 MForm,提供「确定 / 取消」按钮与错误展示区。
|
|
336
161
|
*
|
|
337
|
-
* submitForm 与 validateForm
|
|
162
|
+
* submitForm 与 validateForm 的弹层 UI 完全一致,仅「确定」时调用的实例方法、错误处理与取消文案不同,
|
|
338
163
|
* 这些差异通过 `onConfirm` / `onCancel` 注入,弹层结构在此统一收口。
|
|
339
164
|
*/
|
|
340
|
-
const
|
|
165
|
+
const createDialogWrapper = (options: DialogWrapperOptions): Component => {
|
|
341
166
|
const { formRef, formProps, title, name, onConfirm, onCancel } = options;
|
|
342
167
|
|
|
343
168
|
const btnBase = {
|
|
@@ -461,63 +286,22 @@ const createDebugWrapper = (options: DebugWrapperOptions): Component => {
|
|
|
461
286
|
},
|
|
462
287
|
});
|
|
463
288
|
};
|
|
464
|
-
// #endregion
|
|
289
|
+
// #endregion createDialogWrapper
|
|
465
290
|
|
|
466
291
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
* 类似 ElMessage 的用法:传入 props(包含 `config`/`initValues` 等),函数内部会临时挂载
|
|
470
|
-
* 一个不可见的 Form 组件实例,等待初始化完成后调用其 `submitForm` 方法,
|
|
471
|
-
* 校验通过则 resolve 表单值,校验失败则 reject 错误信息,最后自动卸载实例。
|
|
472
|
-
*
|
|
473
|
-
* @example
|
|
474
|
-
* ```ts
|
|
475
|
-
* import { submitForm } from '@tmagic/form';
|
|
476
|
-
*
|
|
477
|
-
* try {
|
|
478
|
-
* const values = await submitForm({
|
|
479
|
-
* config: [...],
|
|
480
|
-
* initValues: { name: 'foo' },
|
|
481
|
-
* });
|
|
482
|
-
* console.log(values);
|
|
483
|
-
* } catch (e) {
|
|
484
|
-
* console.error(e);
|
|
485
|
-
* }
|
|
292
|
+
* 可见弹层提交:把表单渲染在弹层里,点击「确定」才调用挂载实例的 `submitForm`。
|
|
486
293
|
*
|
|
487
|
-
*
|
|
488
|
-
* const { values, changeRecords } = await submitForm({
|
|
489
|
-
* config: [...],
|
|
490
|
-
* initValues: { name: 'foo' },
|
|
491
|
-
* returnChangeRecords: true,
|
|
492
|
-
* });
|
|
493
|
-
*
|
|
494
|
-
* // 调试模式:可见地渲染表单,点击「确定」才提交:
|
|
495
|
-
* const values = await submitForm({
|
|
496
|
-
* config: [...],
|
|
497
|
-
* initValues: { name: 'foo' },
|
|
498
|
-
* debug: true,
|
|
499
|
-
* });
|
|
500
|
-
* ```
|
|
294
|
+
* 用于需要用户填写/确认的场景;默认路径仍是无渲染校验(见 `submitForm` 的说明)。
|
|
501
295
|
*/
|
|
502
|
-
|
|
503
|
-
const { native, appContext,
|
|
296
|
+
const submitFormByDialogRender = (options: SubmitFormOptions): Promise<any> => {
|
|
297
|
+
const { native, appContext, returnChangeRecords, signal, dialog, title, ...formProps } = options;
|
|
504
298
|
|
|
505
299
|
return mountFormInstance<any>({
|
|
506
300
|
formProps,
|
|
507
301
|
appContext,
|
|
508
|
-
timeout,
|
|
509
302
|
signal,
|
|
510
|
-
// 调试模式需把表单展示出来;普通模式隐藏挂载
|
|
511
|
-
hidden: !debug,
|
|
512
|
-
// 调试模式等待人工操作,不应用超时
|
|
513
|
-
skipTimeout: debug,
|
|
514
|
-
timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
|
|
515
303
|
createWrapper: ({ formRef, formProps, cleanup, resolve, reject }) => {
|
|
516
|
-
|
|
517
|
-
* 执行一次提交:nextTick 等待子组件渲染 → 快照 changeRecords → 调用实例 submitForm → resolve。
|
|
518
|
-
* 校验失败时交给 `onValidateError` 决定「保留弹层展示错误(debug)」还是「reject 并清理(普通)」,
|
|
519
|
-
* 从而让 debug 与普通模式共享同一份提交逻辑。
|
|
520
|
-
*/
|
|
304
|
+
// 执行一次提交:nextTick 等待子组件渲染 → 快照 changeRecords → 调用实例 submitForm → resolve
|
|
521
305
|
const doSubmit = async (onValidateError: (err: any) => void) => {
|
|
522
306
|
try {
|
|
523
307
|
// 等待子组件(FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
|
|
@@ -532,92 +316,25 @@ export const submitForm = (options: SubmitFormOptions): Promise<any> => {
|
|
|
532
316
|
}
|
|
533
317
|
};
|
|
534
318
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
formRef,
|
|
539
|
-
formProps,
|
|
540
|
-
name: 'MFormSubmitWrapper',
|
|
541
|
-
title: 'submitForm 调试',
|
|
542
|
-
onConfirm: (setError) =>
|
|
543
|
-
doSubmit((err) => {
|
|
544
|
-
// 校验失败时保留弹层并展示错误,便于修正后重新提交
|
|
545
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
546
|
-
}),
|
|
547
|
-
onCancel: () => {
|
|
548
|
-
reject(new Error('submitForm canceled in debug mode.'));
|
|
549
|
-
cleanup();
|
|
550
|
-
},
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// 普通模式:表单初始化完成后自动提交
|
|
555
|
-
return defineComponent({
|
|
319
|
+
return createDialogWrapper({
|
|
320
|
+
formRef,
|
|
321
|
+
formProps,
|
|
556
322
|
name: 'MFormSubmitWrapper',
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
});
|
|
567
|
-
},
|
|
568
|
-
{ flush: 'post', immediate: true },
|
|
569
|
-
);
|
|
570
|
-
|
|
571
|
-
return () => h(Form as Component, { ...formProps, ref: formRef });
|
|
323
|
+
title: title ?? 'submitForm',
|
|
324
|
+
onConfirm: (setError) =>
|
|
325
|
+
doSubmit((err) => {
|
|
326
|
+
// 校验失败时保留弹层并展示错误,便于修正后重新提交
|
|
327
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
328
|
+
}),
|
|
329
|
+
onCancel: () => {
|
|
330
|
+
reject(new Error('submitForm canceled.'));
|
|
331
|
+
cleanup();
|
|
572
332
|
},
|
|
573
333
|
});
|
|
574
334
|
},
|
|
575
335
|
});
|
|
576
336
|
};
|
|
577
337
|
|
|
578
|
-
// #region ValidateFormOptions
|
|
579
|
-
/**
|
|
580
|
-
* validateForm 函数参数(与 Form.vue 组件 props 对齐,取校验所需子集)
|
|
581
|
-
*/
|
|
582
|
-
export interface ValidateFormOptions {
|
|
583
|
-
/** 表单配置 */
|
|
584
|
-
config: FormConfig;
|
|
585
|
-
/** 待校验的表单值 */
|
|
586
|
-
initValues?: Record<string, any>;
|
|
587
|
-
parentValues?: Record<string, any>;
|
|
588
|
-
labelWidth?: string;
|
|
589
|
-
keyProp?: string;
|
|
590
|
-
/**
|
|
591
|
-
* 校验失败时,错误提示前缀是否使用字段的 text 文案(通过 `getTextByName` 从 config 中查找)。
|
|
592
|
-
* 默认 `true`,置为 `false` 时直接使用字段 name。
|
|
593
|
-
*/
|
|
594
|
-
useFieldTextInError?: boolean;
|
|
595
|
-
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
|
596
|
-
/**
|
|
597
|
-
* 父级应用上下文,用于继承全局组件、指令、provide 等。
|
|
598
|
-
* 通常通过 `app._context` 或 `getCurrentInstance()?.appContext` 获取。
|
|
599
|
-
*/
|
|
600
|
-
appContext?: AppContext | null;
|
|
601
|
-
/** 等待表单初始化的最长时间(毫秒),超时将以错误 reject。默认 10000ms */
|
|
602
|
-
timeout?: number;
|
|
603
|
-
/**
|
|
604
|
-
* 调试模式。默认 `false`。
|
|
605
|
-
*
|
|
606
|
-
* - `false`:以隐藏方式挂载,初始化完成后自动校验并 resolve 错误文案(原有静默行为)。
|
|
607
|
-
* - `true`:将表单以弹层形式可见地渲染在页面上,需手动点击「确定」才会触发校验,
|
|
608
|
-
* 点击「取消」则以 reject 中断;校验失败时在弹层内展示错误信息并保留弹层,便于修正后重试,
|
|
609
|
-
* 校验通过则 resolve 空字符串。调试模式下 `timeout` 不生效(等待人工操作)。
|
|
610
|
-
*/
|
|
611
|
-
debug?: boolean;
|
|
612
|
-
typeMatchValid?: boolean;
|
|
613
|
-
/**
|
|
614
|
-
* 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
|
|
615
|
-
* 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
|
|
616
|
-
*/
|
|
617
|
-
signal?: AbortSignal;
|
|
618
|
-
}
|
|
619
|
-
// #endregion ValidateFormOptions
|
|
620
|
-
|
|
621
338
|
// #region stripTabLazy
|
|
622
339
|
/**
|
|
623
340
|
* 深拷贝配置值,保留函数(display / onTabClick 等回调)引用,避免破坏配置中的回调。
|
|
@@ -673,44 +390,12 @@ export const stripTabItemsLazy = (config: FormConfig): FormConfig => {
|
|
|
673
390
|
// #endregion stripTabLazy
|
|
674
391
|
|
|
675
392
|
/**
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
* 与 `submitForm` 类似,内部会临时挂载一个不可见的 MForm 实例,等待其初始化完成后调用
|
|
679
|
-
* 实例的 `validate` 方法,返回汇总后的错误文案,随后自动卸载实例。
|
|
680
|
-
*
|
|
681
|
-
* 与 `submitForm` 的区别:
|
|
682
|
-
* - 「静默」:校验失败不抛异常、不触发 `error` 事件、不返回表单值;
|
|
683
|
-
* - 仅用于「探测」配置是否合法,适合源码保存后校验、批量校验组件配置等场景。
|
|
684
|
-
*
|
|
685
|
-
* 由于每次都新建一个独立的 MForm 实例,调用方无需持有任何表单 ref,也不会污染
|
|
686
|
-
* 页面上正在展示的表单状态。
|
|
393
|
+
* 可见弹层校验:把表单渲染在弹层里,点击「确定」才调用挂载实例的 `validate`。
|
|
687
394
|
*
|
|
688
|
-
*
|
|
689
|
-
* 仅在初始化超时或挂载失败等异常情况下才会 reject。
|
|
690
|
-
*
|
|
691
|
-
* @example
|
|
692
|
-
* ```ts
|
|
693
|
-
* import { validateForm } from '@tmagic/form';
|
|
694
|
-
*
|
|
695
|
-
* const error = await validateForm({
|
|
696
|
-
* config: [...],
|
|
697
|
-
* initValues: { name: 'foo' },
|
|
698
|
-
* appContext: getCurrentInstance()?.appContext,
|
|
699
|
-
* });
|
|
700
|
-
* if (error) {
|
|
701
|
-
* // 配置不合法,error 为错误文案
|
|
702
|
-
* }
|
|
703
|
-
*
|
|
704
|
-
* // 调试模式:可见地渲染表单,点击「确定」才校验,校验失败保留弹层可修正重试:
|
|
705
|
-
* const error = await validateForm({
|
|
706
|
-
* config: [...],
|
|
707
|
-
* initValues: { name: 'foo' },
|
|
708
|
-
* debug: true,
|
|
709
|
-
* });
|
|
710
|
-
* ```
|
|
395
|
+
* 用于需要用户填写/确认的场景;默认路径仍是无渲染校验(见 `validateForm` 的说明)。
|
|
711
396
|
*/
|
|
712
|
-
|
|
713
|
-
const { appContext,
|
|
397
|
+
const validateFormByDialogRender = (options: ValidateFormOptions): Promise<string> => {
|
|
398
|
+
const { appContext, config, signal, dialog, title, ...rest } = options;
|
|
714
399
|
|
|
715
400
|
// 去掉 tab 容器各标签页的 lazy,确保懒加载标签页内的字段也参与校验
|
|
716
401
|
const formProps = { ...rest, config: stripTabItemsLazy(config) };
|
|
@@ -718,19 +403,9 @@ export const validateForm = (options: ValidateFormOptions): Promise<string> => {
|
|
|
718
403
|
return mountFormInstance<string>({
|
|
719
404
|
formProps,
|
|
720
405
|
appContext,
|
|
721
|
-
timeout,
|
|
722
406
|
signal,
|
|
723
|
-
// 调试模式需把表单展示出来;普通模式隐藏挂载
|
|
724
|
-
hidden: !debug,
|
|
725
|
-
// 调试模式等待人工操作,不应用超时
|
|
726
|
-
skipTimeout: debug,
|
|
727
|
-
timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
|
|
728
407
|
createWrapper: ({ formRef, formProps, cleanup, resolve, reject }) => {
|
|
729
|
-
|
|
730
|
-
* 执行一次校验:nextTick 等待子组件渲染 → 调用实例 validate → 通过则 resolve ''。
|
|
731
|
-
* 校验失败(返回非空错误文案)时交给 `onInvalid` 决定「静默 resolve 错误文案(普通)」
|
|
732
|
-
* 还是「在弹层展示错误并保留供重试(debug)」,从而让两种模式共享同一份校验逻辑。
|
|
733
|
-
*/
|
|
408
|
+
// 执行一次校验:nextTick 等待子组件渲染 → 调用实例 validate → 通过则 resolve '',失败则在弹层展示错误
|
|
734
409
|
const doValidate = async (onInvalid: (error: string) => void) => {
|
|
735
410
|
try {
|
|
736
411
|
// 等待子组件(FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
|
|
@@ -749,46 +424,43 @@ export const validateForm = (options: ValidateFormOptions): Promise<string> => {
|
|
|
749
424
|
}
|
|
750
425
|
};
|
|
751
426
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
formRef,
|
|
756
|
-
formProps,
|
|
757
|
-
name: 'MFormValidateWrapper',
|
|
758
|
-
title: 'validateForm 调试',
|
|
759
|
-
onConfirm: (setError) =>
|
|
760
|
-
doValidate((error) => {
|
|
761
|
-
// 校验失败时保留弹层并展示错误,便于修正后重新校验
|
|
762
|
-
setError(error);
|
|
763
|
-
}),
|
|
764
|
-
onCancel: () => {
|
|
765
|
-
reject(new Error('validateForm canceled in debug mode.'));
|
|
766
|
-
cleanup();
|
|
767
|
-
},
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// 普通模式:表单初始化完成后自动校验(静默 resolve 错误文案)
|
|
772
|
-
return defineComponent({
|
|
427
|
+
return createDialogWrapper({
|
|
428
|
+
formRef,
|
|
429
|
+
formProps,
|
|
773
430
|
name: 'MFormValidateWrapper',
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
cleanup();
|
|
784
|
-
});
|
|
785
|
-
},
|
|
786
|
-
{ flush: 'post', immediate: true },
|
|
787
|
-
);
|
|
788
|
-
|
|
789
|
-
return () => h(Form as Component, { ...formProps, ref: formRef });
|
|
431
|
+
title: title ?? 'validateForm',
|
|
432
|
+
onConfirm: (setError) =>
|
|
433
|
+
doValidate((error) => {
|
|
434
|
+
// 校验失败时保留弹层并展示错误,便于修正后重新校验
|
|
435
|
+
setError(error);
|
|
436
|
+
}),
|
|
437
|
+
onCancel: () => {
|
|
438
|
+
reject(new Error('validateForm canceled.'));
|
|
439
|
+
cleanup();
|
|
790
440
|
},
|
|
791
441
|
});
|
|
792
442
|
},
|
|
793
443
|
});
|
|
794
444
|
};
|
|
445
|
+
|
|
446
|
+
// #region headless
|
|
447
|
+
/**
|
|
448
|
+
* 以命令式方式对一份「表单配置 + 值」做一次校验并取回表单值。
|
|
449
|
+
*
|
|
450
|
+
* 默认走无渲染实现(见 `@tmagic/form/headless`)。`dialog: true` 会把表单以弹层渲染出来。
|
|
451
|
+
*/
|
|
452
|
+
export const submitForm = async (options: SubmitFormOptions): Promise<any> => {
|
|
453
|
+
if (options.dialog) return submitFormByDialogRender(options);
|
|
454
|
+
return submitFormHeadless(options);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* 以命令式方式对一份「表单配置 + 值」做一次静默校验。
|
|
459
|
+
*
|
|
460
|
+
* 默认走无渲染实现。`dialog: true` 的处理详见 `submitForm`。
|
|
461
|
+
*/
|
|
462
|
+
export const validateForm = async (options: ValidateFormOptions): Promise<string> => {
|
|
463
|
+
if (options.dialog) return validateFormByDialogRender(options);
|
|
464
|
+
return validateFormHeadless(options);
|
|
465
|
+
};
|
|
466
|
+
// #endregion headless
|