@tmagic/form 1.8.0-beta.15 → 1.8.0-beta.17
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 +12 -1
- package/dist/es/style.css +5 -1
- package/dist/es/submitForm.js +73 -51
- package/dist/es/utils/form.js +10 -6
- package/dist/es/utils/typeMatch.js +1 -1
- package/dist/style.css +5 -1
- package/dist/themes/magic-admin.css +11 -1
- package/dist/tmagic-form.umd.cjs +96 -59
- package/package.json +4 -4
- package/src/Form.vue +13 -1
- package/src/submitForm.ts +131 -74
- package/src/theme/group-list.scss +11 -2
- package/src/theme/themes/magic-admin/index.scss +11 -0
- package/src/utils/form.ts +17 -6
- package/src/utils/typeMatch.ts +1 -1
- package/types/index.d.ts +19 -6
|
@@ -145,6 +145,17 @@ var Form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
/**
|
|
148
|
+
* formState 的内置 key 快照(keyProp / values / $emit / fields / post 等)。
|
|
149
|
+
*
|
|
150
|
+
* 在 `extendState` 首次合并前捕获,`applyExtendState` 会据此禁止 `extendState`
|
|
151
|
+
* 覆盖这些已有字段(只能新增字段),避免表单核心状态被外部意外改写。
|
|
152
|
+
*
|
|
153
|
+
* 之所以在此处(effect 之外)捕获而不是在 `applyExtendState` 内动态取:
|
|
154
|
+
* `watchEffect` 会在依赖变化时重跑,若动态取,`extendState` 自己新增的字段在第二次
|
|
155
|
+
* 合并时也会被当成「已有 key」而拒绝刷新;这里只锁定内置字段即可规避该问题。
|
|
156
|
+
*/
|
|
157
|
+
const reservedStateKeys = new Set(Reflect.ownKeys(formState));
|
|
158
|
+
/**
|
|
148
159
|
* `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
|
|
149
160
|
* 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
|
|
150
161
|
*
|
|
@@ -179,7 +190,7 @@ var Form_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
|
|
|
179
190
|
return;
|
|
180
191
|
}
|
|
181
192
|
if (stale) return;
|
|
182
|
-
applyExtendState(formState, state);
|
|
193
|
+
applyExtendState(formState, state, reservedStateKeys);
|
|
183
194
|
});
|
|
184
195
|
provide("mForm", formState);
|
|
185
196
|
/**
|
package/dist/es/style.css
CHANGED
|
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
296
296
|
gap: 8px;
|
|
297
297
|
}
|
|
298
298
|
.m-fields-group-list .m-fields-group-list-footer {
|
|
299
|
-
display: flex;
|
|
300
299
|
justify-content: space-between;
|
|
301
300
|
margin-top: 10px;
|
|
302
301
|
margin-bottom: 16px;
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
305
|
+
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
306
|
+
margin-bottom: 10px;
|
|
307
|
+
}
|
|
308
|
+
|
|
305
309
|
.m-form-panel .el-card__header:hover {
|
|
306
310
|
background: #f2f6fc;
|
|
307
311
|
}
|
package/dist/es/submitForm.js
CHANGED
|
@@ -3,6 +3,8 @@ import { applyExtendState } from "./utils/form.js";
|
|
|
3
3
|
import Form_default from "./Form.js";
|
|
4
4
|
import { createApp, defineComponent, h, nextTick, provide, ref, watch } from "vue";
|
|
5
5
|
//#region packages/form/src/submitForm.ts
|
|
6
|
+
/** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
|
|
7
|
+
var DEFAULT_MOUNT_TIMEOUT = 1e4;
|
|
6
8
|
/**
|
|
7
9
|
* submitForm / validateForm 的公共脚手架:
|
|
8
10
|
*
|
|
@@ -13,14 +15,19 @@ import { createApp, defineComponent, h, nextTick, provide, ref, watch } from "vu
|
|
|
13
15
|
* 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
|
|
14
16
|
*/
|
|
15
17
|
var mountFormInstance = (options) => {
|
|
16
|
-
const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options;
|
|
18
|
+
const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
|
|
17
19
|
return new Promise((resolve, reject) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
if (signal?.aborted) {
|
|
21
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
21
24
|
let cleaned = false;
|
|
22
25
|
let timer = null;
|
|
26
|
+
let onAbort = null;
|
|
23
27
|
const instance = { app: null };
|
|
28
|
+
const container = document.createElement("div");
|
|
29
|
+
if (hidden) container.style.display = "none";
|
|
30
|
+
document.body.appendChild(container);
|
|
24
31
|
const cleanup = () => {
|
|
25
32
|
if (cleaned) return;
|
|
26
33
|
cleaned = true;
|
|
@@ -28,58 +35,71 @@ var mountFormInstance = (options) => {
|
|
|
28
35
|
clearTimeout(timer);
|
|
29
36
|
timer = null;
|
|
30
37
|
}
|
|
38
|
+
if (signal && onAbort) {
|
|
39
|
+
signal.removeEventListener("abort", onAbort);
|
|
40
|
+
onAbort = null;
|
|
41
|
+
}
|
|
31
42
|
try {
|
|
32
43
|
instance.app?.unmount();
|
|
33
44
|
} catch {}
|
|
34
45
|
container.parentNode?.removeChild(container);
|
|
35
46
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
formProps: restFormProps,
|
|
41
|
-
cleanup,
|
|
42
|
-
resolve,
|
|
43
|
-
reject
|
|
44
|
-
});
|
|
45
|
-
const wrapperComponent = typeof extendState === "function" ? defineComponent({
|
|
46
|
-
name: "MFormExtendStateInjector",
|
|
47
|
-
setup() {
|
|
48
|
-
watch(() => formRef.value, (form) => {
|
|
49
|
-
if (!form) return;
|
|
50
|
-
let result;
|
|
51
|
-
try {
|
|
52
|
-
result = extendState(form.formState);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
console.error("[MForm] extendState failed:", e);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const apply = (state) => applyExtendState(form.formState, state);
|
|
58
|
-
if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
|
|
59
|
-
else apply(result);
|
|
60
|
-
}, {
|
|
61
|
-
flush: "sync",
|
|
62
|
-
immediate: true
|
|
63
|
-
});
|
|
64
|
-
return () => h(userWrapper);
|
|
65
|
-
}
|
|
66
|
-
}) : userWrapper;
|
|
67
|
-
const app = createApp(hidden ? defineComponent({
|
|
68
|
-
name: "MFormSilentProvider",
|
|
69
|
-
setup() {
|
|
70
|
-
provide(FORM_SILENT_MODE_KEY, true);
|
|
71
|
-
return () => h(wrapperComponent);
|
|
72
|
-
}
|
|
73
|
-
}) : wrapperComponent);
|
|
74
|
-
instance.app = app;
|
|
75
|
-
if (appContext) Object.assign(app._context, appContext);
|
|
76
|
-
if (timeout > 0 && !skipTimeout) timer = setTimeout(() => {
|
|
77
|
-
if (!cleaned) {
|
|
78
|
-
reject(new Error(timeoutMessage));
|
|
47
|
+
if (signal) {
|
|
48
|
+
onAbort = () => {
|
|
49
|
+
if (cleaned) return;
|
|
50
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
|
|
79
51
|
cleanup();
|
|
80
|
-
}
|
|
81
|
-
|
|
52
|
+
};
|
|
53
|
+
signal.addEventListener("abort", onAbort);
|
|
54
|
+
}
|
|
82
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);
|
|
83
103
|
app.mount(container);
|
|
84
104
|
} catch (err) {
|
|
85
105
|
reject(err);
|
|
@@ -212,11 +232,12 @@ var createDebugWrapper = (options) => {
|
|
|
212
232
|
* ```
|
|
213
233
|
*/
|
|
214
234
|
var submitForm = (options) => {
|
|
215
|
-
const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, ...formProps } = options;
|
|
235
|
+
const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
|
|
216
236
|
return mountFormInstance({
|
|
217
237
|
formProps,
|
|
218
238
|
appContext,
|
|
219
239
|
timeout,
|
|
240
|
+
signal,
|
|
220
241
|
hidden: !debug,
|
|
221
242
|
skipTimeout: debug,
|
|
222
243
|
timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
|
|
@@ -353,7 +374,7 @@ var stripTabItemsLazy = (config) => {
|
|
|
353
374
|
* ```
|
|
354
375
|
*/
|
|
355
376
|
var validateForm = (options) => {
|
|
356
|
-
const { appContext, timeout = 1e4, debug = false, config, ...rest } = options;
|
|
377
|
+
const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
|
|
357
378
|
return mountFormInstance({
|
|
358
379
|
formProps: {
|
|
359
380
|
...rest,
|
|
@@ -361,6 +382,7 @@ var validateForm = (options) => {
|
|
|
361
382
|
},
|
|
362
383
|
appContext,
|
|
363
384
|
timeout,
|
|
385
|
+
signal,
|
|
364
386
|
hidden: !debug,
|
|
365
387
|
skipTimeout: debug,
|
|
366
388
|
timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
|
package/dist/es/utils/form.js
CHANGED
|
@@ -211,15 +211,19 @@ var sortChange = (data, { prop, order }) => {
|
|
|
211
211
|
* - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
|
|
212
212
|
* 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
|
|
213
213
|
*
|
|
214
|
-
* 注意:
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
214
|
+
* 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
|
|
215
|
+
* 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
|
|
216
|
+
* config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
|
|
217
|
+
* post 等),命中这些 key 时统一跳过并告警。
|
|
218
|
+
*
|
|
219
|
+
* 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
|
|
220
|
+
* 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
|
|
221
|
+
* `TypeError: 'set' on proxy: trap returned falsish`。
|
|
219
222
|
*/
|
|
220
|
-
var applyExtendState = (formState, state) => {
|
|
223
|
+
var applyExtendState = (formState, state, reservedKeys) => {
|
|
221
224
|
if (!state) return;
|
|
222
225
|
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
|
|
226
|
+
if (reservedKeys?.has(key)) continue;
|
|
223
227
|
if (!("value" in descriptor)) {
|
|
224
228
|
descriptor.configurable = true;
|
|
225
229
|
Object.defineProperty(formState, key, descriptor);
|
|
@@ -89,7 +89,7 @@ var stringifyExampleValue = (value) => {
|
|
|
89
89
|
}
|
|
90
90
|
return String(value);
|
|
91
91
|
};
|
|
92
|
-
var MAX_SUGGESTION_OPTIONS =
|
|
92
|
+
var MAX_SUGGESTION_OPTIONS = 20;
|
|
93
93
|
/**
|
|
94
94
|
* 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
|
|
95
95
|
* 可选值超过 MAX_SUGGESTION_OPTIONS 个时仅展示前若干个并以「等」省略。
|
package/dist/style.css
CHANGED
|
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
296
296
|
gap: 8px;
|
|
297
297
|
}
|
|
298
298
|
.m-fields-group-list .m-fields-group-list-footer {
|
|
299
|
-
display: flex;
|
|
300
299
|
justify-content: space-between;
|
|
301
300
|
margin-top: 10px;
|
|
302
301
|
margin-bottom: 16px;
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
305
|
+
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
306
|
+
margin-bottom: 10px;
|
|
307
|
+
}
|
|
308
|
+
|
|
305
309
|
.m-form-panel .el-card__header:hover {
|
|
306
310
|
background: #f2f6fc;
|
|
307
311
|
}
|
|
@@ -296,12 +296,16 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
296
296
|
gap: 8px;
|
|
297
297
|
}
|
|
298
298
|
.m-fields-group-list .m-fields-group-list-footer {
|
|
299
|
-
display: flex;
|
|
300
299
|
justify-content: space-between;
|
|
301
300
|
margin-top: 10px;
|
|
302
301
|
margin-bottom: 16px;
|
|
303
302
|
}
|
|
304
303
|
|
|
304
|
+
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
305
|
+
.m-container-group-list.outer-gorup_list > .m-fields-group-list > .m-fields-group-list-item {
|
|
306
|
+
margin-bottom: 10px;
|
|
307
|
+
}
|
|
308
|
+
|
|
305
309
|
.m-form-panel .el-card__header:hover {
|
|
306
310
|
background: #f2f6fc;
|
|
307
311
|
}
|
|
@@ -496,4 +500,10 @@ fieldset.m-fieldset .m-form-tip {
|
|
|
496
500
|
}
|
|
497
501
|
.m-form.m-form--magic-admin .m-form-container .m-form-container-expand {
|
|
498
502
|
text-align: left;
|
|
503
|
+
}
|
|
504
|
+
.m-form.m-form--magic-admin .m-fields-group-list .el-table__empty-block .el-table__empty-text {
|
|
505
|
+
width: 100%;
|
|
506
|
+
background-color: rgba(0, 0, 0, 0.03);
|
|
507
|
+
margin-top: 8px;
|
|
508
|
+
border-radius: 4px;
|
|
499
509
|
}
|
package/dist/tmagic-form.umd.cjs
CHANGED
|
@@ -2816,7 +2816,7 @@
|
|
|
2816
2816
|
}
|
|
2817
2817
|
return String(value);
|
|
2818
2818
|
};
|
|
2819
|
-
var MAX_SUGGESTION_OPTIONS =
|
|
2819
|
+
var MAX_SUGGESTION_OPTIONS = 20;
|
|
2820
2820
|
/**
|
|
2821
2821
|
* 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
|
|
2822
2822
|
* 可选值超过 MAX_SUGGESTION_OPTIONS 个时仅展示前若干个并以「等」省略。
|
|
@@ -3340,15 +3340,19 @@
|
|
|
3340
3340
|
* - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
|
|
3341
3341
|
* 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
|
|
3342
3342
|
*
|
|
3343
|
-
* 注意:
|
|
3344
|
-
*
|
|
3345
|
-
*
|
|
3346
|
-
*
|
|
3347
|
-
*
|
|
3343
|
+
* 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
|
|
3344
|
+
* 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
|
|
3345
|
+
* config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
|
|
3346
|
+
* post 等),命中这些 key 时统一跳过并告警。
|
|
3347
|
+
*
|
|
3348
|
+
* 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
|
|
3349
|
+
* 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
|
|
3350
|
+
* `TypeError: 'set' on proxy: trap returned falsish`。
|
|
3348
3351
|
*/
|
|
3349
|
-
var applyExtendState = (formState, state) => {
|
|
3352
|
+
var applyExtendState = (formState, state, reservedKeys) => {
|
|
3350
3353
|
if (!state) return;
|
|
3351
3354
|
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
|
|
3355
|
+
if (reservedKeys?.has(key)) continue;
|
|
3352
3356
|
if (!("value" in descriptor)) {
|
|
3353
3357
|
descriptor.configurable = true;
|
|
3354
3358
|
Object.defineProperty(formState, key, descriptor);
|
|
@@ -4167,6 +4171,17 @@
|
|
|
4167
4171
|
}
|
|
4168
4172
|
});
|
|
4169
4173
|
/**
|
|
4174
|
+
* formState 的内置 key 快照(keyProp / values / $emit / fields / post 等)。
|
|
4175
|
+
*
|
|
4176
|
+
* 在 `extendState` 首次合并前捕获,`applyExtendState` 会据此禁止 `extendState`
|
|
4177
|
+
* 覆盖这些已有字段(只能新增字段),避免表单核心状态被外部意外改写。
|
|
4178
|
+
*
|
|
4179
|
+
* 之所以在此处(effect 之外)捕获而不是在 `applyExtendState` 内动态取:
|
|
4180
|
+
* `watchEffect` 会在依赖变化时重跑,若动态取,`extendState` 自己新增的字段在第二次
|
|
4181
|
+
* 合并时也会被当成「已有 key」而拒绝刷新;这里只锁定内置字段即可规避该问题。
|
|
4182
|
+
*/
|
|
4183
|
+
const reservedStateKeys = new Set(Reflect.ownKeys(formState));
|
|
4184
|
+
/**
|
|
4170
4185
|
* `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
|
|
4171
4186
|
* 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
|
|
4172
4187
|
*
|
|
@@ -4201,7 +4216,7 @@
|
|
|
4201
4216
|
return;
|
|
4202
4217
|
}
|
|
4203
4218
|
if (stale) return;
|
|
4204
|
-
applyExtendState(formState, state);
|
|
4219
|
+
applyExtendState(formState, state, reservedStateKeys);
|
|
4205
4220
|
});
|
|
4206
4221
|
(0, vue.provide)("mForm", formState);
|
|
4207
4222
|
/**
|
|
@@ -4405,6 +4420,8 @@
|
|
|
4405
4420
|
});
|
|
4406
4421
|
//#endregion
|
|
4407
4422
|
//#region packages/form/src/submitForm.ts
|
|
4423
|
+
/** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
|
|
4424
|
+
var DEFAULT_MOUNT_TIMEOUT = 1e4;
|
|
4408
4425
|
/**
|
|
4409
4426
|
* submitForm / validateForm 的公共脚手架:
|
|
4410
4427
|
*
|
|
@@ -4415,14 +4432,19 @@
|
|
|
4415
4432
|
* 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
|
|
4416
4433
|
*/
|
|
4417
4434
|
var mountFormInstance = (options) => {
|
|
4418
|
-
const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options;
|
|
4435
|
+
const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, signal, createWrapper } = options;
|
|
4419
4436
|
return new Promise((resolve, reject) => {
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4437
|
+
if (signal?.aborted) {
|
|
4438
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4423
4441
|
let cleaned = false;
|
|
4424
4442
|
let timer = null;
|
|
4443
|
+
let onAbort = null;
|
|
4425
4444
|
const instance = { app: null };
|
|
4445
|
+
const container = document.createElement("div");
|
|
4446
|
+
if (hidden) container.style.display = "none";
|
|
4447
|
+
document.body.appendChild(container);
|
|
4426
4448
|
const cleanup = () => {
|
|
4427
4449
|
if (cleaned) return;
|
|
4428
4450
|
cleaned = true;
|
|
@@ -4430,58 +4452,71 @@
|
|
|
4430
4452
|
clearTimeout(timer);
|
|
4431
4453
|
timer = null;
|
|
4432
4454
|
}
|
|
4455
|
+
if (signal && onAbort) {
|
|
4456
|
+
signal.removeEventListener("abort", onAbort);
|
|
4457
|
+
onAbort = null;
|
|
4458
|
+
}
|
|
4433
4459
|
try {
|
|
4434
4460
|
instance.app?.unmount();
|
|
4435
4461
|
} catch {}
|
|
4436
4462
|
container.parentNode?.removeChild(container);
|
|
4437
4463
|
};
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
formProps: restFormProps,
|
|
4443
|
-
cleanup,
|
|
4444
|
-
resolve,
|
|
4445
|
-
reject
|
|
4446
|
-
});
|
|
4447
|
-
const wrapperComponent = typeof extendState === "function" ? (0, vue.defineComponent)({
|
|
4448
|
-
name: "MFormExtendStateInjector",
|
|
4449
|
-
setup() {
|
|
4450
|
-
(0, vue.watch)(() => formRef.value, (form) => {
|
|
4451
|
-
if (!form) return;
|
|
4452
|
-
let result;
|
|
4453
|
-
try {
|
|
4454
|
-
result = extendState(form.formState);
|
|
4455
|
-
} catch (e) {
|
|
4456
|
-
console.error("[MForm] extendState failed:", e);
|
|
4457
|
-
return;
|
|
4458
|
-
}
|
|
4459
|
-
const apply = (state) => applyExtendState(form.formState, state);
|
|
4460
|
-
if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
|
|
4461
|
-
else apply(result);
|
|
4462
|
-
}, {
|
|
4463
|
-
flush: "sync",
|
|
4464
|
-
immediate: true
|
|
4465
|
-
});
|
|
4466
|
-
return () => (0, vue.h)(userWrapper);
|
|
4467
|
-
}
|
|
4468
|
-
}) : userWrapper;
|
|
4469
|
-
const app = (0, vue.createApp)(hidden ? (0, vue.defineComponent)({
|
|
4470
|
-
name: "MFormSilentProvider",
|
|
4471
|
-
setup() {
|
|
4472
|
-
(0, vue.provide)(FORM_SILENT_MODE_KEY, true);
|
|
4473
|
-
return () => (0, vue.h)(wrapperComponent);
|
|
4474
|
-
}
|
|
4475
|
-
}) : wrapperComponent);
|
|
4476
|
-
instance.app = app;
|
|
4477
|
-
if (appContext) Object.assign(app._context, appContext);
|
|
4478
|
-
if (timeout > 0 && !skipTimeout) timer = setTimeout(() => {
|
|
4479
|
-
if (!cleaned) {
|
|
4480
|
-
reject(new Error(timeoutMessage));
|
|
4464
|
+
if (signal) {
|
|
4465
|
+
onAbort = () => {
|
|
4466
|
+
if (cleaned) return;
|
|
4467
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("mountFormInstance aborted"));
|
|
4481
4468
|
cleanup();
|
|
4482
|
-
}
|
|
4483
|
-
|
|
4469
|
+
};
|
|
4470
|
+
signal.addEventListener("abort", onAbort);
|
|
4471
|
+
}
|
|
4484
4472
|
try {
|
|
4473
|
+
const formRef = (0, vue.ref)(null);
|
|
4474
|
+
const { extendState, ...restFormProps } = formProps;
|
|
4475
|
+
const userWrapper = createWrapper({
|
|
4476
|
+
formRef,
|
|
4477
|
+
formProps: restFormProps,
|
|
4478
|
+
cleanup,
|
|
4479
|
+
resolve,
|
|
4480
|
+
reject
|
|
4481
|
+
});
|
|
4482
|
+
const wrapperComponent = typeof extendState === "function" ? (0, vue.defineComponent)({
|
|
4483
|
+
name: "MFormExtendStateInjector",
|
|
4484
|
+
setup() {
|
|
4485
|
+
(0, vue.watch)(() => formRef.value, (form) => {
|
|
4486
|
+
if (!form) return;
|
|
4487
|
+
let result;
|
|
4488
|
+
try {
|
|
4489
|
+
result = extendState(form.formState);
|
|
4490
|
+
} catch (e) {
|
|
4491
|
+
console.error("[MForm] extendState failed:", e);
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
4494
|
+
const reservedStateKeys = new Set(Reflect.ownKeys(form.formState));
|
|
4495
|
+
const apply = (state) => applyExtendState(form.formState, state, reservedStateKeys);
|
|
4496
|
+
if (result && typeof result.then === "function") result.then(apply, (e) => console.error("[MForm] extendState failed:", e));
|
|
4497
|
+
else apply(result);
|
|
4498
|
+
}, {
|
|
4499
|
+
flush: "sync",
|
|
4500
|
+
immediate: true
|
|
4501
|
+
});
|
|
4502
|
+
return () => (0, vue.h)(userWrapper);
|
|
4503
|
+
}
|
|
4504
|
+
}) : userWrapper;
|
|
4505
|
+
const app = (0, vue.createApp)(hidden ? (0, vue.defineComponent)({
|
|
4506
|
+
name: "MFormSilentProvider",
|
|
4507
|
+
setup() {
|
|
4508
|
+
(0, vue.provide)(FORM_SILENT_MODE_KEY, true);
|
|
4509
|
+
return () => (0, vue.h)(wrapperComponent);
|
|
4510
|
+
}
|
|
4511
|
+
}) : wrapperComponent);
|
|
4512
|
+
instance.app = app;
|
|
4513
|
+
if (appContext) Object.assign(app._context, appContext);
|
|
4514
|
+
if (!skipTimeout) timer = setTimeout(() => {
|
|
4515
|
+
if (!cleaned) {
|
|
4516
|
+
reject(new Error(timeoutMessage));
|
|
4517
|
+
cleanup();
|
|
4518
|
+
}
|
|
4519
|
+
}, timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT);
|
|
4485
4520
|
app.mount(container);
|
|
4486
4521
|
} catch (err) {
|
|
4487
4522
|
reject(err);
|
|
@@ -4614,11 +4649,12 @@
|
|
|
4614
4649
|
* ```
|
|
4615
4650
|
*/
|
|
4616
4651
|
var submitForm = (options) => {
|
|
4617
|
-
const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, ...formProps } = options;
|
|
4652
|
+
const { native, appContext, timeout = 1e4, returnChangeRecords, debug = false, signal, ...formProps } = options;
|
|
4618
4653
|
return mountFormInstance({
|
|
4619
4654
|
formProps,
|
|
4620
4655
|
appContext,
|
|
4621
4656
|
timeout,
|
|
4657
|
+
signal,
|
|
4622
4658
|
hidden: !debug,
|
|
4623
4659
|
skipTimeout: debug,
|
|
4624
4660
|
timeoutMessage: `submitForm timeout after ${timeout}ms: form is not initialized.`,
|
|
@@ -4755,7 +4791,7 @@
|
|
|
4755
4791
|
* ```
|
|
4756
4792
|
*/
|
|
4757
4793
|
var validateForm = (options) => {
|
|
4758
|
-
const { appContext, timeout = 1e4, debug = false, config, ...rest } = options;
|
|
4794
|
+
const { appContext, timeout = 1e4, debug = false, config, signal, ...rest } = options;
|
|
4759
4795
|
return mountFormInstance({
|
|
4760
4796
|
formProps: {
|
|
4761
4797
|
...rest,
|
|
@@ -4763,6 +4799,7 @@
|
|
|
4763
4799
|
},
|
|
4764
4800
|
appContext,
|
|
4765
4801
|
timeout,
|
|
4802
|
+
signal,
|
|
4766
4803
|
hidden: !debug,
|
|
4767
4804
|
skipTimeout: debug,
|
|
4768
4805
|
timeoutMessage: `validateForm timeout after ${timeout}ms: form is not initialized.`,
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.8.0-beta.
|
|
2
|
+
"version": "1.8.0-beta.17",
|
|
3
3
|
"name": "@tmagic/form",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"vue": "^3.5.40",
|
|
54
54
|
"typescript": "^6.0.3",
|
|
55
|
-
"@tmagic/design": "1.8.0-beta.
|
|
56
|
-
"@tmagic/
|
|
57
|
-
"@tmagic/
|
|
55
|
+
"@tmagic/design": "1.8.0-beta.17",
|
|
56
|
+
"@tmagic/utils": "1.8.0-beta.17",
|
|
57
|
+
"@tmagic/form-schema": "1.8.0-beta.17"
|
|
58
58
|
},
|
|
59
59
|
"peerDependenciesMeta": {
|
|
60
60
|
"typescript": {
|
package/src/Form.vue
CHANGED
|
@@ -261,6 +261,18 @@ const formState: FormState = reactive<FormState>({
|
|
|
261
261
|
},
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* formState 的内置 key 快照(keyProp / values / $emit / fields / post 等)。
|
|
266
|
+
*
|
|
267
|
+
* 在 `extendState` 首次合并前捕获,`applyExtendState` 会据此禁止 `extendState`
|
|
268
|
+
* 覆盖这些已有字段(只能新增字段),避免表单核心状态被外部意外改写。
|
|
269
|
+
*
|
|
270
|
+
* 之所以在此处(effect 之外)捕获而不是在 `applyExtendState` 内动态取:
|
|
271
|
+
* `watchEffect` 会在依赖变化时重跑,若动态取,`extendState` 自己新增的字段在第二次
|
|
272
|
+
* 合并时也会被当成「已有 key」而拒绝刷新;这里只锁定内置字段即可规避该问题。
|
|
273
|
+
*/
|
|
274
|
+
const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(formState));
|
|
275
|
+
|
|
264
276
|
/**
|
|
265
277
|
* `extendState` 的同步段(直到第一个 `await` 之前)所访问的任何响应式数据,
|
|
266
278
|
* 都会被 `watchEffect` 自动跟踪。这样可以兼容历史用法 ——
|
|
@@ -299,7 +311,7 @@ watchEffect(async (onCleanup) => {
|
|
|
299
311
|
}
|
|
300
312
|
if (stale) return;
|
|
301
313
|
|
|
302
|
-
applyExtendState(formState, state);
|
|
314
|
+
applyExtendState(formState, state, reservedStateKeys);
|
|
303
315
|
});
|
|
304
316
|
|
|
305
317
|
provide('mForm', formState);
|
package/src/submitForm.ts
CHANGED
|
@@ -87,6 +87,11 @@ export interface SubmitFormOptions {
|
|
|
87
87
|
*/
|
|
88
88
|
debug?: boolean;
|
|
89
89
|
typeMatchValid?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
|
|
92
|
+
* 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
|
|
93
|
+
*/
|
|
94
|
+
signal?: AbortSignal;
|
|
90
95
|
}
|
|
91
96
|
// #endregion SubmitFormOptions
|
|
92
97
|
|
|
@@ -127,7 +132,7 @@ interface MountFormInstanceOptions<T> {
|
|
|
127
132
|
formProps: Record<string, any>;
|
|
128
133
|
/** 父级应用上下文,用于继承全局组件、指令、provide 等 */
|
|
129
134
|
appContext?: AppContext | null;
|
|
130
|
-
/** 等待表单初始化的最长时间(毫秒),<=0
|
|
135
|
+
/** 等待表单初始化的最长时间(毫秒),<=0 时回退到默认超时以保证兜底清理生效 */
|
|
131
136
|
timeout: number;
|
|
132
137
|
/** 超时 reject 的错误文案 */
|
|
133
138
|
timeoutMessage: string;
|
|
@@ -135,10 +140,15 @@ interface MountFormInstanceOptions<T> {
|
|
|
135
140
|
hidden?: boolean;
|
|
136
141
|
/** 是否跳过超时注册。调试模式等待人工操作,应传 `true` */
|
|
137
142
|
skipTimeout?: boolean;
|
|
143
|
+
/** 外部中断信号:abort 时会 reject 并卸载实例、移除容器,用于取消无超时(如 debug)的挂载 */
|
|
144
|
+
signal?: AbortSignal;
|
|
138
145
|
/** 构造 wrapper 组件 */
|
|
139
146
|
createWrapper: FormWrapperFactory<T>;
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
/** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
|
|
150
|
+
const DEFAULT_MOUNT_TIMEOUT = 10000;
|
|
151
|
+
|
|
142
152
|
/**
|
|
143
153
|
* submitForm / validateForm 的公共脚手架:
|
|
144
154
|
*
|
|
@@ -149,20 +159,36 @@ interface MountFormInstanceOptions<T> {
|
|
|
149
159
|
* 容器创建、卸载、超时、上下文注入等模板代码在此统一收口。
|
|
150
160
|
*/
|
|
151
161
|
const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T> => {
|
|
152
|
-
const {
|
|
162
|
+
const {
|
|
163
|
+
formProps,
|
|
164
|
+
appContext,
|
|
165
|
+
timeout,
|
|
166
|
+
timeoutMessage,
|
|
167
|
+
hidden = true,
|
|
168
|
+
skipTimeout = false,
|
|
169
|
+
signal,
|
|
170
|
+
createWrapper,
|
|
171
|
+
} = options;
|
|
153
172
|
|
|
154
173
|
return new Promise<T>((resolve, reject) => {
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
174
|
+
// 已中断则直接 reject,不创建任何容器/实例
|
|
175
|
+
if (signal?.aborted) {
|
|
176
|
+
reject(signal.reason ?? new Error('mountFormInstance aborted'));
|
|
177
|
+
return;
|
|
158
178
|
}
|
|
159
|
-
document.body.appendChild(container);
|
|
160
179
|
|
|
161
180
|
let cleaned = false;
|
|
162
181
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
182
|
+
let onAbort: (() => void) | null = null;
|
|
163
183
|
// 用 holder 持有 app,使 cleanup 可在 app 创建之前定义(const app + 无 TDZ / 无 use-before-define)
|
|
164
184
|
const instance: { app: ReturnType<typeof createApp> | null } = { app: null };
|
|
165
185
|
|
|
186
|
+
const container = document.createElement('div');
|
|
187
|
+
if (hidden) {
|
|
188
|
+
container.style.display = 'none';
|
|
189
|
+
}
|
|
190
|
+
document.body.appendChild(container);
|
|
191
|
+
|
|
166
192
|
const cleanup = () => {
|
|
167
193
|
if (cleaned) return;
|
|
168
194
|
cleaned = true;
|
|
@@ -170,6 +196,10 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
|
|
|
170
196
|
clearTimeout(timer);
|
|
171
197
|
timer = null;
|
|
172
198
|
}
|
|
199
|
+
if (signal && onAbort) {
|
|
200
|
+
signal.removeEventListener('abort', onAbort);
|
|
201
|
+
onAbort = null;
|
|
202
|
+
}
|
|
173
203
|
try {
|
|
174
204
|
instance.app?.unmount();
|
|
175
205
|
} catch {
|
|
@@ -178,81 +208,101 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
|
|
|
178
208
|
container.parentNode?.removeChild(container);
|
|
179
209
|
};
|
|
180
210
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
211
|
+
// 支持外部通过 AbortSignal 主动中断:debug 模式无超时兜底,若调用方放弃了该 Promise,
|
|
212
|
+
// 可通过 abort 卸载实例、移除遮罩/容器,避免无限驻留在 DOM 中。
|
|
213
|
+
if (signal) {
|
|
214
|
+
onAbort = () => {
|
|
215
|
+
if (cleaned) return;
|
|
216
|
+
reject(signal.reason ?? new Error('mountFormInstance aborted'));
|
|
217
|
+
cleanup();
|
|
218
|
+
};
|
|
219
|
+
signal.addEventListener('abort', onAbort);
|
|
220
|
+
}
|
|
190
221
|
|
|
191
|
-
|
|
192
|
-
|
|
222
|
+
// 从容器创建到 mount 的全流程统一 try/catch:任一步骤(createWrapper/createApp/上下文合并/mount)
|
|
223
|
+
// 抛错都会走到 cleanup,避免已插入 body 的 container 及未挂载的 app 残留导致泄漏。
|
|
224
|
+
try {
|
|
225
|
+
const formRef = ref<any>(null);
|
|
226
|
+
|
|
227
|
+
// 将 extendState 从 formProps 中剥离:不由 Form.vue 的 async watchEffect 异步应用,
|
|
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
|
|
193
277
|
? defineComponent({
|
|
194
|
-
name: '
|
|
278
|
+
name: 'MFormSilentProvider',
|
|
195
279
|
setup() {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
(form) => {
|
|
199
|
-
if (!form) return;
|
|
200
|
-
let result: any;
|
|
201
|
-
try {
|
|
202
|
-
result = extendState(form.formState);
|
|
203
|
-
} catch (e) {
|
|
204
|
-
console.error('[MForm] extendState failed:', e);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
// 合并逻辑收口在 applyExtendState:props 派生的只读 getter 字段
|
|
208
|
-
// (keyProp 等)以普通字段形式返回时会被跳过并告警,避免 proxy set 抛错
|
|
209
|
-
const apply = (state: Record<string, any> | null | undefined) =>
|
|
210
|
-
applyExtendState(form.formState, state);
|
|
211
|
-
if (result && typeof result.then === 'function') {
|
|
212
|
-
result.then(apply, (e: any) => console.error('[MForm] extendState failed:', e));
|
|
213
|
-
} else {
|
|
214
|
-
apply(result);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
{ flush: 'sync', immediate: true },
|
|
218
|
-
);
|
|
219
|
-
return () => h(userWrapper);
|
|
280
|
+
provide(FORM_SILENT_MODE_KEY, true);
|
|
281
|
+
return () => h(wrapperComponent);
|
|
220
282
|
},
|
|
221
283
|
})
|
|
222
|
-
:
|
|
223
|
-
|
|
224
|
-
// 静默(隐藏挂载)模式下注入静默标记:vs-code 等重型字段组件可据此跳过自身渲染,
|
|
225
|
-
// 校验/取值依赖 FormItem 与 model 值,与叶子 UI 组件无关(见 FORM_SILENT_MODE_KEY 注释)。
|
|
226
|
-
// 用组件级 provide 而非 app.provide:appContext 合并后 app._context.provides 与父级应用
|
|
227
|
-
// 共享引用,app.provide 会把标记泄漏到父级应用。
|
|
228
|
-
const rootComponent = hidden
|
|
229
|
-
? defineComponent({
|
|
230
|
-
name: 'MFormSilentProvider',
|
|
231
|
-
setup() {
|
|
232
|
-
provide(FORM_SILENT_MODE_KEY, true);
|
|
233
|
-
return () => h(wrapperComponent);
|
|
234
|
-
},
|
|
235
|
-
})
|
|
236
|
-
: wrapperComponent;
|
|
284
|
+
: wrapperComponent;
|
|
237
285
|
|
|
238
|
-
|
|
239
|
-
|
|
286
|
+
const app = createApp(rootComponent);
|
|
287
|
+
instance.app = app;
|
|
240
288
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
289
|
+
// 继承父级应用上下文(components / directives / provides / config 等)
|
|
290
|
+
if (appContext) {
|
|
291
|
+
Object.assign(app._context, appContext);
|
|
292
|
+
}
|
|
245
293
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
}
|
|
254
305
|
|
|
255
|
-
try {
|
|
256
306
|
app.mount(container);
|
|
257
307
|
} catch (err) {
|
|
258
308
|
reject(err);
|
|
@@ -450,12 +500,13 @@ const createDebugWrapper = (options: DebugWrapperOptions): Component => {
|
|
|
450
500
|
* ```
|
|
451
501
|
*/
|
|
452
502
|
export const submitForm = (options: SubmitFormOptions): Promise<any> => {
|
|
453
|
-
const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, ...formProps } = options;
|
|
503
|
+
const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, signal, ...formProps } = options;
|
|
454
504
|
|
|
455
505
|
return mountFormInstance<any>({
|
|
456
506
|
formProps,
|
|
457
507
|
appContext,
|
|
458
508
|
timeout,
|
|
509
|
+
signal,
|
|
459
510
|
// 调试模式需把表单展示出来;普通模式隐藏挂载
|
|
460
511
|
hidden: !debug,
|
|
461
512
|
// 调试模式等待人工操作,不应用超时
|
|
@@ -559,6 +610,11 @@ export interface ValidateFormOptions {
|
|
|
559
610
|
*/
|
|
560
611
|
debug?: boolean;
|
|
561
612
|
typeMatchValid?: boolean;
|
|
613
|
+
/**
|
|
614
|
+
* 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
|
|
615
|
+
* 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
|
|
616
|
+
*/
|
|
617
|
+
signal?: AbortSignal;
|
|
562
618
|
}
|
|
563
619
|
// #endregion ValidateFormOptions
|
|
564
620
|
|
|
@@ -654,7 +710,7 @@ export const stripTabItemsLazy = (config: FormConfig): FormConfig => {
|
|
|
654
710
|
* ```
|
|
655
711
|
*/
|
|
656
712
|
export const validateForm = (options: ValidateFormOptions): Promise<string> => {
|
|
657
|
-
const { appContext, timeout = 10000, debug = false, config, ...rest } = options;
|
|
713
|
+
const { appContext, timeout = 10000, debug = false, config, signal, ...rest } = options;
|
|
658
714
|
|
|
659
715
|
// 去掉 tab 容器各标签页的 lazy,确保懒加载标签页内的字段也参与校验
|
|
660
716
|
const formProps = { ...rest, config: stripTabItemsLazy(config) };
|
|
@@ -663,6 +719,7 @@ export const validateForm = (options: ValidateFormOptions): Promise<string> => {
|
|
|
663
719
|
formProps,
|
|
664
720
|
appContext,
|
|
665
721
|
timeout,
|
|
722
|
+
signal,
|
|
666
723
|
// 调试模式需把表单展示出来;普通模式隐藏挂载
|
|
667
724
|
hidden: !debug,
|
|
668
725
|
// 调试模式等待人工操作,不应用超时
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
.m-fields-group-list-item.tmagic-design-card--flat:last-child {
|
|
3
3
|
border: 0;
|
|
4
4
|
}
|
|
5
|
-
|
|
6
5
|
.el-button--text {
|
|
7
6
|
padding: 0;
|
|
8
7
|
margin-bottom: 7px;
|
|
@@ -32,9 +31,19 @@
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
.m-fields-group-list-footer {
|
|
35
|
-
display: flex;
|
|
36
34
|
justify-content: space-between;
|
|
37
35
|
margin-top: 10px;
|
|
38
36
|
margin-bottom: 16px;
|
|
39
37
|
}
|
|
40
38
|
}
|
|
39
|
+
|
|
40
|
+
/** 最外层的groupList需要每个item需要增加空白区域界限时,增加outer-gorup_list可以实现 */
|
|
41
|
+
.m-container-group-list {
|
|
42
|
+
&.outer-gorup_list {
|
|
43
|
+
> .m-fields-group-list {
|
|
44
|
+
> .m-fields-group-list-item {
|
|
45
|
+
margin-bottom: 10px;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/utils/form.ts
CHANGED
|
@@ -449,16 +449,27 @@ export const sortChange = (data: any[], { prop, order }: SortProp) => {
|
|
|
449
449
|
* - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
|
|
450
450
|
* 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
|
|
451
451
|
*
|
|
452
|
-
* 注意:
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
452
|
+
* 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
|
|
453
|
+
* 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
|
|
454
|
+
* config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
|
|
455
|
+
* post 等),命中这些 key 时统一跳过并告警。
|
|
456
|
+
*
|
|
457
|
+
* 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
|
|
458
|
+
* 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
|
|
459
|
+
* `TypeError: 'set' on proxy: trap returned falsish`。
|
|
457
460
|
*/
|
|
458
|
-
export const applyExtendState = (
|
|
461
|
+
export const applyExtendState = (
|
|
462
|
+
formState: FormState,
|
|
463
|
+
state: Record<string, any> | null | undefined,
|
|
464
|
+
reservedKeys?: Set<string | symbol>,
|
|
465
|
+
): void => {
|
|
459
466
|
if (!state) return;
|
|
460
467
|
|
|
461
468
|
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
|
|
469
|
+
if (reservedKeys?.has(key)) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
462
473
|
if (!('value' in descriptor)) {
|
|
463
474
|
descriptor.configurable = true;
|
|
464
475
|
Object.defineProperty(formState, key, descriptor);
|
package/src/utils/typeMatch.ts
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -128,6 +128,11 @@ interface SubmitFormOptions {
|
|
|
128
128
|
*/
|
|
129
129
|
debug?: boolean;
|
|
130
130
|
typeMatchValid?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
|
|
133
|
+
* 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
|
|
134
|
+
*/
|
|
135
|
+
signal?: AbortSignal;
|
|
131
136
|
}
|
|
132
137
|
/**
|
|
133
138
|
* 开启 `returnChangeNodes` 时 submitForm 的返回结果
|
|
@@ -209,6 +214,11 @@ interface ValidateFormOptions {
|
|
|
209
214
|
*/
|
|
210
215
|
debug?: boolean;
|
|
211
216
|
typeMatchValid?: boolean;
|
|
217
|
+
/**
|
|
218
|
+
* 外部中断信号。abort 时会立即以 `signal.reason` reject 并卸载临时表单实例、移除容器。
|
|
219
|
+
* 主要用于 `debug` 模式(无超时兜底)下取消一个被放弃的表单弹层,避免其无限驻留在页面上。
|
|
220
|
+
*/
|
|
221
|
+
signal?: AbortSignal;
|
|
212
222
|
}
|
|
213
223
|
/**
|
|
214
224
|
* 返回一份去除了 tab 标签页 lazy 的配置副本。
|
|
@@ -290,13 +300,16 @@ declare const sortChange: (data: any[], {
|
|
|
290
300
|
* - accessor 描述符(`{ get stage() { return ... } }`)按原样 defineProperty,调用方
|
|
291
301
|
* 可控制读时求值;强制 `configurable: true` 以便下一次合并可再 define。
|
|
292
302
|
*
|
|
293
|
-
* 注意:
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
303
|
+
* 注意:extendState 只能向 formState「新增」字段,不允许覆盖其已有 key。
|
|
304
|
+
* 调用方可通过 `reservedKeys` 传入合并前已存在的内置 key 快照(keyProp / popperClass /
|
|
305
|
+
* config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
|
|
306
|
+
* post 等),命中这些 key 时统一跳过并告警。
|
|
307
|
+
*
|
|
308
|
+
* 兜底:未传 `reservedKeys` 时,仍会拦截 props 派生的只读 getter 字段(无 setter),
|
|
309
|
+
* 否则以普通字段形式赋值会让 proxy 的 set trap 抛出
|
|
310
|
+
* `TypeError: 'set' on proxy: trap returned falsish`。
|
|
298
311
|
*/
|
|
299
|
-
declare const applyExtendState: (formState: schema_d_exports.FormState, state: Record<string, any> | null | undefined) => void;
|
|
312
|
+
declare const applyExtendState: (formState: schema_d_exports.FormState, state: Record<string, any> | null | undefined, reservedKeys?: Set<string | symbol>) => void;
|
|
300
313
|
declare const createObjectProp: (prop: string, key: string, name?: string | number) => string;
|
|
301
314
|
//#endregion
|
|
302
315
|
//#region temp/packages/form/src/utils/useAddField.d.ts
|