@tmagic/form 1.8.0-manmanyu.9 → 1.8.1
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 +88 -107
- package/dist/es/FormBox.vue_vue_type_script_setup_true_lang.js +21 -5
- package/dist/es/FormDialog.vue_vue_type_script_setup_true_lang.js +21 -5
- package/dist/es/FormDrawer.vue_vue_type_script_setup_true_lang.js +46 -12
- package/dist/es/containers/Col.vue_vue_type_script_setup_true_lang.js +6 -2
- package/dist/es/containers/Container.vue_vue_type_script_setup_true_lang.js +44 -47
- package/dist/es/containers/Fieldset.vue_vue_type_script_setup_true_lang.js +7 -1
- package/dist/es/containers/FlexLayout.vue_vue_type_script_setup_true_lang.js +5 -2
- 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 +59 -22
- package/dist/es/containers/GroupListItem.vue_vue_type_script_setup_true_lang.js +118 -100
- package/dist/es/containers/Panel.vue_vue_type_script_setup_true_lang.js +8 -3
- package/dist/es/containers/Row.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/containers/Step.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/containers/Tabs.vue_vue_type_script_setup_true_lang.js +8 -1
- package/dist/es/containers/table/ActionsColumn.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/table/SortColumn.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/containers/table/Table.vue_vue_type_script_setup_true_lang.js +14 -10
- package/dist/es/containers/table/useTableColumns.js +10 -15
- package/dist/es/containers/table-group-list/TableGroupList.vue_vue_type_script_setup_true_lang.js +72 -79
- 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 +2 -2
- package/dist/es/fields/Checkbox.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/ColorPicker.vue_vue_type_script_setup_true_lang.js +1 -1
- 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/Daterange.vue_vue_type_script_setup_true_lang.js +2 -2
- 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/Hidden.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Link.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/es/fields/Number.vue_vue_type_script_setup_true_lang.js +4 -2
- 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/RadioGroup.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/Select.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/Switch.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Text.vue_vue_type_script_setup_true_lang.js +13 -10
- package/dist/es/fields/Textarea.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Time.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/Timerange.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/headless.js +13 -0
- package/dist/es/index.js +19 -12
- package/dist/es/plugin.js +57 -39
- package/dist/es/schema.js +11 -2
- package/dist/es/style.css +123 -3
- package/dist/es/submitForm.js +270 -72
- package/dist/es/utils/builtInFields.js +49 -0
- package/dist/es/utils/collectFields.js +407 -0
- package/dist/es/utils/config.js +4 -8
- package/dist/es/utils/fieldInnerConfig.js +42 -0
- package/dist/es/utils/fieldValueEffects.js +80 -0
- package/dist/es/utils/form.js +154 -19
- 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 +104 -0
- package/dist/es/utils/typeMatch.js +532 -0
- package/dist/es/utils/validateError.js +57 -0
- package/dist/es/utils/validateValues.js +115 -0
- package/dist/style.css +123 -3
- package/dist/themes/magic-admin.css +576 -14
- package/dist/tmagic-form-headless.umd.cjs +4515 -0
- package/dist/tmagic-form.umd.cjs +3444 -983
- package/package.json +16 -10
- package/src/Form.vue +113 -129
- package/src/FormBox.vue +16 -4
- package/src/FormDialog.vue +16 -5
- package/src/FormDrawer.vue +41 -13
- package/src/containers/Col.vue +3 -0
- package/src/containers/Container.vue +40 -45
- package/src/containers/Fieldset.vue +5 -0
- package/src/containers/FlexLayout.vue +2 -0
- package/src/containers/GroupList.vue +64 -13
- package/src/containers/GroupListItem.vue +88 -81
- 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 +5 -3
- package/src/containers/table/usePagination.ts +1 -1
- package/src/containers/table/useSortable.ts +1 -1
- package/src/containers/table/useTableColumns.ts +15 -22
- package/src/containers/table-group-list/TableGroupList.vue +46 -72
- 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/Link.vue +3 -4
- package/src/fields/Number.vue +2 -1
- package/src/fields/{NumberRange.vue → NumberRange/Index.vue} +2 -6
- package/src/fields/NumberRange/effect.ts +27 -0
- package/src/fields/RadioGroup.vue +13 -1
- package/src/fields/Select.vue +13 -4
- package/src/headless.ts +78 -0
- package/src/index.ts +46 -9
- package/src/plugin.ts +68 -37
- package/src/schema.ts +13 -2
- package/src/submitForm.ts +410 -131
- package/src/theme/fieldset.scss +1 -0
- package/src/theme/form-box.scss +5 -0
- package/src/theme/form-dialog.scss +1 -1
- package/src/theme/group-list.scss +143 -2
- package/src/theme/table.scss +11 -1
- package/src/theme/themes/magic-admin/index.scss +27 -16
- package/src/utils/builtInFields.ts +69 -0
- package/src/utils/collectFields.ts +561 -0
- package/src/utils/config.ts +7 -15
- package/src/utils/fieldInnerConfig.ts +127 -0
- package/src/utils/fieldValueEffects.ts +140 -0
- package/src/utils/form.ts +230 -30
- 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 +147 -0
- package/src/utils/typeMatch.ts +948 -0
- 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 +900 -180
- package/dist/es/_virtual/_rolldown/runtime.js +0 -27
- 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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2025 Tencent. All rights reserved.
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { toLine } from '@tmagic/utils';
|
|
20
|
+
|
|
21
|
+
import type { FormItemConfig, FormState, FormValue } from '../schema';
|
|
22
|
+
|
|
23
|
+
// #region FieldInnerConfig
|
|
24
|
+
/** innerConfig 回调的入参:字段自身的配置、所在层级的 model、完整字段路径与表单状态 */
|
|
25
|
+
export interface FieldInnerConfigContext {
|
|
26
|
+
/** 字段自身的配置(已经过 filterFunction 之外的原样配置) */
|
|
27
|
+
config: FormItemConfig;
|
|
28
|
+
/** 字段所在层级的 model 切片 */
|
|
29
|
+
model: FormValue;
|
|
30
|
+
/** 字段的完整 prop 路径(含父级前缀),即字段组件拿到的 `props.prop` */
|
|
31
|
+
prop: string;
|
|
32
|
+
/** 字段所在层级的 prop 基准(父级 Container 的 `props.prop`),即 `prop` 去掉自身 name 的部分 */
|
|
33
|
+
parentProp: string;
|
|
34
|
+
/** 表单状态 */
|
|
35
|
+
mForm: FormState | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** innerConfig 回调的返回值:需要继续遍历的内部配置及其 model / prop 基准 */
|
|
39
|
+
export interface FieldInnerConfigResult {
|
|
40
|
+
/** 内部配置(对应字段组件内部渲染的 `MContainer` 的 `config`) */
|
|
41
|
+
config: FormItemConfig | FormItemConfig[];
|
|
42
|
+
/**
|
|
43
|
+
* 内部配置对应的 model 切片,默认沿用字段所在层级的 `model`。
|
|
44
|
+
*
|
|
45
|
+
* 例如 `code-select` 内部是 `:model="model[name]"`,就应返回 `model[config.name]`。
|
|
46
|
+
*/
|
|
47
|
+
model?: FormValue;
|
|
48
|
+
/**
|
|
49
|
+
* 内部配置对应的 prop 基准,默认沿用字段自身的 `prop`。
|
|
50
|
+
*
|
|
51
|
+
* 返回的 config 的 `name` 会被追加到这个基准上。所以当内部配置复用了字段自身的 `name`
|
|
52
|
+
* (如 `display-conds` 内部的 group-list 就叫 `props.name`)时,要返回 `parentProp`,
|
|
53
|
+
* 否则 name 会被拼两次。
|
|
54
|
+
*/
|
|
55
|
+
prop?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 复合字段的内部配置回调。
|
|
60
|
+
*
|
|
61
|
+
* 有一类字段组件会在自身内部再渲染 `MContainer` 并传入组件内部临时算出来的 config
|
|
62
|
+
* (如编辑器的 `code-select` / `event-select` / `style-setter`),这些内部字段同样会向
|
|
63
|
+
* 父级表单注册 FormItem、参与父表单校验。无渲染校验(`validateValues`)只遍历调用方
|
|
64
|
+
* 传入的 config 树,看不到这些运行期才产生的配置,因此需要字段作者通过
|
|
65
|
+
* `registerField(type, { innerConfig })` 把内部配置交出来。
|
|
66
|
+
*
|
|
67
|
+
* 返回 `null` / `undefined` 表示该字段本次没有内部配置(例如某些分支下不渲染子表单)。
|
|
68
|
+
*
|
|
69
|
+
* 既未登记 innerConfig、也不在叶子字段表里的 type:配置里有 `items` 会下钻子项,
|
|
70
|
+
* 自身有 `rules` 会校验自身。
|
|
71
|
+
*/
|
|
72
|
+
export type FieldInnerConfig = (_ctx: FieldInnerConfigContext) => FieldInnerConfigResult | null | undefined | void;
|
|
73
|
+
// #endregion FieldInnerConfig
|
|
74
|
+
|
|
75
|
+
/** 内置内部配置(由 `registerBuiltInFields` 写入;`clearFields` 不会清掉) */
|
|
76
|
+
const builtInInnerConfigs = new Map<string, FieldInnerConfig>();
|
|
77
|
+
/** 业务侧登记的内部配置 */
|
|
78
|
+
const extraInnerConfigs = new Map<string, FieldInnerConfig>();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 登记复合字段的内部配置:无渲染校验遇到该 type 时,用返回值继续遍历内部字段。
|
|
82
|
+
*
|
|
83
|
+
* `type` 会按 Container 的规则归一化为中划线形式(`codeSelect` 与 `code-select` 等价)。
|
|
84
|
+
* 重复登记以最后一次为准,便于业务侧覆盖内置实现。
|
|
85
|
+
* `builtIn` 登记不受 `deleteFieldInnerConfig` / `clearFieldInnerConfigs` 影响。
|
|
86
|
+
*
|
|
87
|
+
* @param type - 字段 type
|
|
88
|
+
* @param resolve - innerConfig 回调
|
|
89
|
+
* @param [builtIn=false] - 是否写入内置表
|
|
90
|
+
*/
|
|
91
|
+
export const registerFieldInnerConfig = (type: string, resolve: FieldInnerConfig, builtIn = false): void => {
|
|
92
|
+
if (typeof type !== 'string' || !type || typeof resolve !== 'function') return;
|
|
93
|
+
(builtIn ? builtInInnerConfigs : extraInnerConfigs).set(toLine(type), resolve);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 获取指定 type 的 innerConfig 回调(业务侧优先于内置)。
|
|
98
|
+
*
|
|
99
|
+
* @param type - 字段 type
|
|
100
|
+
* @returns innerConfig 回调;未登记则为 `undefined`
|
|
101
|
+
*/
|
|
102
|
+
export const getFieldInnerConfig = (type: string): FieldInnerConfig | undefined => {
|
|
103
|
+
const key = toLine(type);
|
|
104
|
+
return extraInnerConfigs.get(key) ?? builtInInnerConfigs.get(key);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 是否已登记指定 type 的 innerConfig(内置 ∪ 已登记)。
|
|
109
|
+
*
|
|
110
|
+
* @param type - 字段 type
|
|
111
|
+
* @returns 是否已登记
|
|
112
|
+
*/
|
|
113
|
+
export const hasFieldInnerConfig = (type: string): boolean => {
|
|
114
|
+
const key = toLine(type);
|
|
115
|
+
return extraInnerConfigs.has(key) || builtInInnerConfigs.has(key);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 删除业务侧登记的 innerConfig(不影响内置)。
|
|
120
|
+
*
|
|
121
|
+
* @param type - 字段 type
|
|
122
|
+
* @returns 是否删除成功
|
|
123
|
+
*/
|
|
124
|
+
export const deleteFieldInnerConfig = (type: string): boolean => extraInnerConfigs.delete(toLine(type));
|
|
125
|
+
|
|
126
|
+
/** 清空业务侧登记的 innerConfig(不影响内置;主要用于单测)。 */
|
|
127
|
+
export const clearFieldInnerConfigs = (): void => extraInnerConfigs.clear();
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2025 Tencent. All rights reserved.
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @fileoverview 叶子字段登记表:哪些 type 是叶子,以及该字段对表单值的初始化写入。
|
|
21
|
+
*
|
|
22
|
+
* 无渲染校验据此判定该 type 没有属于父表单的嵌套字段。
|
|
23
|
+
* 业务自定义字段通过 `registerField` / `registerFields` 登记。
|
|
24
|
+
* 内置叶子字段由 `MagicForm.install` 的 `registerBuiltInFields` 写入。
|
|
25
|
+
* 未登记且没有 innerConfig 的 type:有 `items` 会下钻子项,自身有 `rules` 会校验自身。
|
|
26
|
+
*
|
|
27
|
+
* 这里只是登记表。各字段的 effect 写在对应组件同目录的 `fields/<Field>/effect.ts`,
|
|
28
|
+
* 执行收口在 `collectFields` 的 `applyMountValueEffects`:渲染(`Form.vue`)
|
|
29
|
+
* 与无渲染(`validateValues`)都在表单值初始化完成后调用它一次,
|
|
30
|
+
* 字段组件自身不再在 setup 里改写 model。
|
|
31
|
+
*
|
|
32
|
+
* @module fieldValueEffects
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { toLine } from '@tmagic/utils';
|
|
36
|
+
|
|
37
|
+
import type { FormItemConfig, FormState, FormValue } from '../schema';
|
|
38
|
+
|
|
39
|
+
// #region FieldMountValueEffect
|
|
40
|
+
/** effect 的入参:字段自身的配置、所在层级的 model、完整字段路径、表单值根对象与表单状态 */
|
|
41
|
+
export interface FieldMountValueEffectContext {
|
|
42
|
+
/** 字段自身的配置 */
|
|
43
|
+
config: FormItemConfig;
|
|
44
|
+
/** 字段所在层级的 model 切片 */
|
|
45
|
+
model: FormValue;
|
|
46
|
+
/** 字段的完整 prop 路径(含父级前缀),对应 Container 的 `itemProp` */
|
|
47
|
+
prop: string;
|
|
48
|
+
/**
|
|
49
|
+
* 本次处理的表单值根对象,`prop` 即以它为根。
|
|
50
|
+
*
|
|
51
|
+
* 需要按路径跨层级写值时用它,不要用 `mForm.values`:对比模式下处理的是 lastValues 那一份,
|
|
52
|
+
* tab / table 新增行处理的则是还没挂到表单上的一行值,两者都与 `mForm.values` 不是同一个对象。
|
|
53
|
+
*/
|
|
54
|
+
values: FormValue;
|
|
55
|
+
/** 表单状态 */
|
|
56
|
+
mForm: FormState | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 字段对表单值的初始化写入(如 `display` 的 `initValue`、`date` 的格式归一化)。
|
|
61
|
+
*
|
|
62
|
+
* 由 `applyMountValueEffects` 在表单值初始化完成后统一执行一次,渲染与无渲染共用。
|
|
63
|
+
* 因为可能对同一份值重复执行(如 `initValues` 变化后重新初始化),实现必须幂等。
|
|
64
|
+
*/
|
|
65
|
+
export type FieldMountValueEffect = (_ctx: FieldMountValueEffectContext) => void;
|
|
66
|
+
// #endregion FieldMountValueEffect
|
|
67
|
+
|
|
68
|
+
/** 内置叶子字段(由 `MagicForm.install` 写入;clearFields 不会清掉) */
|
|
69
|
+
const builtInLeafFieldTypes = new Set<string>();
|
|
70
|
+
const builtInMountValueEffects = new Map<string, FieldMountValueEffect>();
|
|
71
|
+
|
|
72
|
+
/** 业务侧登记的叶子字段与其挂载副作用 */
|
|
73
|
+
const extraLeafFieldTypes = new Set<string>();
|
|
74
|
+
const extraMountValueEffects = new Map<string, FieldMountValueEffect>();
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 登记一个叶子字段 type。由 `registerField` 调用,业务侧请走 `registerField`。
|
|
78
|
+
*
|
|
79
|
+
* 传了 `effect` 会覆盖该 type 已有的 extra effect(含覆盖内置);不传则清掉 extra effect,
|
|
80
|
+
* 内置 effect 仍生效。
|
|
81
|
+
*
|
|
82
|
+
* @param type - 字段 type
|
|
83
|
+
* @param [effect] - 挂载时改写 model 的副作用
|
|
84
|
+
* @param [builtIn=false] - 是否写入内置表
|
|
85
|
+
*/
|
|
86
|
+
export const registerLeafFieldType = (type: string, effect?: FieldMountValueEffect, builtIn = false): void => {
|
|
87
|
+
if (typeof type !== 'string' || !type) return;
|
|
88
|
+
|
|
89
|
+
const key = toLine(type);
|
|
90
|
+
const types = builtIn ? builtInLeafFieldTypes : extraLeafFieldTypes;
|
|
91
|
+
const effects = builtIn ? builtInMountValueEffects : extraMountValueEffects;
|
|
92
|
+
|
|
93
|
+
types.add(key);
|
|
94
|
+
|
|
95
|
+
if (typeof effect === 'function') {
|
|
96
|
+
effects.set(key, effect);
|
|
97
|
+
} else if (!builtIn) {
|
|
98
|
+
effects.delete(key);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 是否为叶子字段 type(内置 ∪ 已登记)。
|
|
104
|
+
*
|
|
105
|
+
* @param type - 字段 type
|
|
106
|
+
* @returns 是否为叶子
|
|
107
|
+
*/
|
|
108
|
+
export const isLeafFieldType = (type: string): boolean => {
|
|
109
|
+
const key = toLine(type);
|
|
110
|
+
return extraLeafFieldTypes.has(key) || builtInLeafFieldTypes.has(key);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 获取指定 type 挂载时改写 model 的副作用(已登记的优先于内置)。
|
|
115
|
+
*
|
|
116
|
+
* @param type - 字段 type
|
|
117
|
+
* @returns 副作用函数;未登记则为 `undefined`
|
|
118
|
+
*/
|
|
119
|
+
export const getFieldMountValueEffect = (type: string): FieldMountValueEffect | undefined => {
|
|
120
|
+
const key = toLine(type);
|
|
121
|
+
return extraMountValueEffects.get(key) ?? builtInMountValueEffects.get(key);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 删除业务侧登记的叶子字段(不影响内置;主要用于单测)。
|
|
126
|
+
*
|
|
127
|
+
* @param type - 字段 type
|
|
128
|
+
* @returns 是否删除成功
|
|
129
|
+
*/
|
|
130
|
+
export const deleteLeafFieldType = (type: string): boolean => {
|
|
131
|
+
const key = toLine(type);
|
|
132
|
+
extraMountValueEffects.delete(key);
|
|
133
|
+
return extraLeafFieldTypes.delete(key);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** 清空业务侧登记的叶子字段(不影响内置;主要用于单测)。 */
|
|
137
|
+
export const clearLeafFieldTypes = (): void => {
|
|
138
|
+
extraLeafFieldTypes.clear();
|
|
139
|
+
extraMountValueEffects.clear();
|
|
140
|
+
};
|
package/src/utils/form.ts
CHANGED
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { readonly } from 'vue';
|
|
19
|
+
import { type MaybeRef, readonly, unref } from 'vue';
|
|
20
20
|
import dayjs from 'dayjs';
|
|
21
|
-
|
|
21
|
+
// dayjs 没有 exports 映射,原生 Node ESM 不会补扩展名,深路径必须写全 .js
|
|
22
|
+
import utc from 'dayjs/plugin/utc.js';
|
|
22
23
|
import { cloneDeep } from 'lodash-es';
|
|
23
24
|
|
|
25
|
+
import { getDesignConfig } from '@tmagic/design/headless';
|
|
24
26
|
import { getValueByKeyPath } from '@tmagic/utils';
|
|
25
27
|
|
|
26
28
|
import type {
|
|
@@ -39,6 +41,64 @@ import type {
|
|
|
39
41
|
TypeFunction,
|
|
40
42
|
} from '../schema';
|
|
41
43
|
|
|
44
|
+
import { getConfig } from './config';
|
|
45
|
+
import { createTypeMatchValidator } from './typeMatch';
|
|
46
|
+
|
|
47
|
+
export { createFormStateProxy, mergeFormContexts } from './formStateProxy';
|
|
48
|
+
|
|
49
|
+
type AsyncValidatorFn = (rule: any, value: any, callback: Function, source?: any, options?: any) => any;
|
|
50
|
+
|
|
51
|
+
const isTDesignAdapter = () => getDesignConfig('adapterType') === 'tdesign-vue-next';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 将 async-validator(Element Plus)风格的 validator 适配到当前 UI 库。
|
|
55
|
+
* TDesign 调用签名为 `(val) => boolean | CustomValidateObj | Promise`,无 callback。
|
|
56
|
+
*/
|
|
57
|
+
export const adaptFormValidator = (validator: AsyncValidatorFn): AsyncValidatorFn => {
|
|
58
|
+
return (arg1: any, arg2?: any, arg3?: any, arg4?: any, arg5?: any) => {
|
|
59
|
+
if (!isTDesignAdapter()) {
|
|
60
|
+
return validator(arg1, arg2, arg3, arg4, arg5);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// TDesign: validator(val)
|
|
64
|
+
const value = arg1;
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
let settled = false;
|
|
67
|
+
const callback = (error?: Error | string | (Error | string)[]) => {
|
|
68
|
+
if (settled) return;
|
|
69
|
+
settled = true;
|
|
70
|
+
// async-validator 约定 callback 也可接收错误数组,TDesign 只能展示一条,取首条
|
|
71
|
+
const first = Array.isArray(error) ? error[0] : error;
|
|
72
|
+
if (first) {
|
|
73
|
+
resolve({
|
|
74
|
+
result: false,
|
|
75
|
+
message: typeof first === 'string' ? first : first.message,
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
resolve(true);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// 异步 validator 会先返回 undefined,稍后再调 callback,这里不能当成 thenable 取值
|
|
84
|
+
const result = validator(undefined, value, callback);
|
|
85
|
+
if (result && typeof (result as PromiseLike<unknown>).then === 'function') {
|
|
86
|
+
Promise.resolve(result).then(
|
|
87
|
+
() => {
|
|
88
|
+
if (!settled) callback();
|
|
89
|
+
},
|
|
90
|
+
(err) => {
|
|
91
|
+
callback(err instanceof Error ? err : new Error(String(err)));
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
callback(e instanceof Error ? e : new Error(String(e)));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
42
102
|
interface DefaultItem {
|
|
43
103
|
defaultValue: any;
|
|
44
104
|
type: string;
|
|
@@ -216,17 +276,106 @@ const getDefaultValue = function (
|
|
|
216
276
|
return '';
|
|
217
277
|
};
|
|
218
278
|
|
|
279
|
+
/**
|
|
280
|
+
* formState 中与表单 props 无关的公共部分:字段注册表、消息组件、post 请求。
|
|
281
|
+
*
|
|
282
|
+
* 由渲染式表单(`Form.vue`)与无渲染校验(`createHeadlessFormState`)共用,
|
|
283
|
+
* 保证两者提供给字段配置(`onChange` / `validator` / 异步 options 等)的能力一致。
|
|
284
|
+
*/
|
|
285
|
+
const fallbackMessage = {
|
|
286
|
+
error: (msg: string) => console.error(msg),
|
|
287
|
+
success: (msg: string) => console.log(msg),
|
|
288
|
+
warning: (msg: string) => console.warn(msg),
|
|
289
|
+
info: (msg: string) => console.info(msg),
|
|
290
|
+
closeAll: () => undefined,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const fallbackMessageBox = {
|
|
294
|
+
alert: (msg: string) => console.log(msg),
|
|
295
|
+
confirm: (msg: string) => console.log(msg),
|
|
296
|
+
close: (msg: string) => console.log(msg),
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export const createFormStateBase = (ui?: { $message?: any; $messageBox?: any }) => {
|
|
300
|
+
const fields = new Map<string, any>();
|
|
301
|
+
const requestFuc = getConfig('request') as Function;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
fields,
|
|
305
|
+
setField: (prop: string, field: any) => fields.set(prop, field),
|
|
306
|
+
getField: (prop: string) => fields.get(prop),
|
|
307
|
+
deleteField: (prop: string) => fields.delete(prop),
|
|
308
|
+
$messageBox: ui?.$messageBox ?? fallbackMessageBox,
|
|
309
|
+
$message: ui?.$message ?? fallbackMessage,
|
|
310
|
+
post: (options: any) => {
|
|
311
|
+
if (requestFuc) {
|
|
312
|
+
return requestFuc({
|
|
313
|
+
method: 'POST',
|
|
314
|
+
...options,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 配置项的 `name` 是否可用于从 model 中下钻取值。
|
|
323
|
+
*
|
|
324
|
+
* 供 `Container.vue` 与无渲染校验(`collectValidatableFields`)共用,两者对「该不该下钻」
|
|
325
|
+
* 的判断必须一致。
|
|
326
|
+
*/
|
|
327
|
+
export const isValidName = (name: unknown): boolean => {
|
|
328
|
+
const valueType = typeof name;
|
|
329
|
+
if (valueType !== 'string' && valueType !== 'symbol' && valueType !== 'number') return false;
|
|
330
|
+
if (name === '') return false;
|
|
331
|
+
if (valueType === 'number') return (name as number) >= 0;
|
|
332
|
+
return true;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 在 `prop` 后追加一段路径。
|
|
337
|
+
*
|
|
338
|
+
* 用于容器把 `prop` 透传给子级时再拼上索引 / key(如 group-list 的行、table 的行)。
|
|
339
|
+
*/
|
|
340
|
+
export const appendProp = (prop: string | undefined = '', key: string | number): string =>
|
|
341
|
+
`${prop}${prop ? '.' : ''}${key}`;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 由父级 `prop` 与配置项 `name` 拼出该配置项的完整 `prop`(即 FormItem 的 prop)。
|
|
345
|
+
*
|
|
346
|
+
* 调用方传入的 name 已按 `config.name || ''` 归一化,空 name 表示该配置项不占据值路径上的一层,
|
|
347
|
+
* 此时沿用父级 `prop`。
|
|
348
|
+
*/
|
|
349
|
+
export const getItemProp = (prop: string | undefined = '', name: string | number | undefined = ''): string =>
|
|
350
|
+
name === '' ? (prop ?? '') : appendProp(prop, name);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 归一化配置项的 `type`:求值动态 type、剥离容器语义的 type、驼峰转中划线、补默认值。
|
|
354
|
+
*
|
|
355
|
+
* 供 `Container.vue` 与无渲染校验共用,保证两者分派到的 type 完全一致。
|
|
356
|
+
*/
|
|
357
|
+
export const resolveItemType = (mForm: FormState | undefined, config: any, props: any): string => {
|
|
358
|
+
let type = 'type' in (config || {}) ? config.type : '';
|
|
359
|
+
type = type && filterFunction<string>(mForm, type, props);
|
|
360
|
+
// form / container 都表示「仅嵌套,不渲染字段」
|
|
361
|
+
if (type === 'form' || type === 'container') return '';
|
|
362
|
+
return type?.replace(/([A-Z])/g, '-$1').toLowerCase() || (config?.items ? '' : 'text');
|
|
363
|
+
};
|
|
364
|
+
|
|
219
365
|
export const filterFunction = <T = any>(
|
|
220
366
|
mForm: FormState | undefined,
|
|
221
367
|
config: T | FilterFunction<T> | undefined,
|
|
222
368
|
props: any,
|
|
223
369
|
) => {
|
|
224
370
|
if (typeof config === 'function') {
|
|
371
|
+
const formValue = readonly(mForm?.values || props.model);
|
|
225
372
|
return (config as FilterFunction<T>)(mForm, {
|
|
226
373
|
values: readonly(mForm?.initValues || {}),
|
|
227
374
|
model: readonly(props.model),
|
|
228
375
|
parent: readonly(mForm?.parentValues || {}),
|
|
229
|
-
formValue
|
|
376
|
+
formValue,
|
|
377
|
+
// Select.vue / SelectOptionFunction 使用 formValues 别名
|
|
378
|
+
formValues: formValue,
|
|
230
379
|
prop: props.prop,
|
|
231
380
|
config: props.config,
|
|
232
381
|
index: props.index,
|
|
@@ -253,39 +402,90 @@ export const display = function (mForm: FormState | undefined, config: any, prop
|
|
|
253
402
|
return true;
|
|
254
403
|
};
|
|
255
404
|
|
|
256
|
-
|
|
257
|
-
|
|
405
|
+
const buildRules = function (
|
|
406
|
+
mForm: FormState | undefined,
|
|
407
|
+
r: Rule[] | Rule = [],
|
|
408
|
+
props: any,
|
|
409
|
+
typeMatchValid?: MaybeRef<boolean>,
|
|
410
|
+
adapt: (_validator: AsyncValidatorFn) => AsyncValidatorFn = (validator) => validator,
|
|
411
|
+
) {
|
|
412
|
+
let rules = cloneDeep(r);
|
|
258
413
|
|
|
259
414
|
if (typeof rules === 'object' && !Array.isArray(rules)) {
|
|
260
415
|
rules = [rules];
|
|
261
416
|
}
|
|
262
417
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
418
|
+
if (unref(typeMatchValid) && !rules.some((r) => typeof r.typeMatch !== 'undefined')) {
|
|
419
|
+
rules.push({
|
|
420
|
+
typeMatch: true,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return rules
|
|
425
|
+
.map((item) => {
|
|
426
|
+
if (item.typeMatch) {
|
|
427
|
+
(item as any).validator = adapt(createTypeMatchValidator(mForm, props, item));
|
|
428
|
+
return item;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (typeof item.validator === 'function') {
|
|
432
|
+
const fnc = item.validator;
|
|
433
|
+
|
|
434
|
+
(item as any).validator = adapt((rule: any, value: any, callback: Function, source: any, options: any) =>
|
|
435
|
+
fnc(
|
|
436
|
+
{
|
|
437
|
+
rule,
|
|
438
|
+
value: props.config.names ? props.model : value,
|
|
439
|
+
callback,
|
|
440
|
+
source,
|
|
441
|
+
options,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
values: mForm?.initValues || {},
|
|
445
|
+
model: props.model,
|
|
446
|
+
parent: mForm?.parentValues || {},
|
|
447
|
+
formValue: mForm?.values || props.model,
|
|
448
|
+
prop: props.prop,
|
|
449
|
+
config: props.config,
|
|
450
|
+
},
|
|
451
|
+
mForm,
|
|
452
|
+
),
|
|
285
453
|
);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
454
|
+
}
|
|
455
|
+
return item;
|
|
456
|
+
})
|
|
457
|
+
.filter((item) => {
|
|
458
|
+
// typeMatch: false 仅用于关闭自动注入,本身没有校验能力。
|
|
459
|
+
// 若原样交给 async-validator,会因默认 type=string 误杀 number 等合法值。
|
|
460
|
+
if (item.typeMatch === false && typeof item.validator !== 'function') {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
export const getRules = function (
|
|
468
|
+
mForm: FormState | undefined,
|
|
469
|
+
r: Rule[] | Rule = [],
|
|
470
|
+
props: any,
|
|
471
|
+
typeMatchValid?: MaybeRef<boolean>,
|
|
472
|
+
) {
|
|
473
|
+
return buildRules(mForm, r, props, typeMatchValid, adaptFormValidator);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* 与 `getRules` 相同,但 validator 保持 async-validator(Element Plus)原生签名,不做 UI 库适配。
|
|
478
|
+
*
|
|
479
|
+
* 供无渲染校验(`validateValues`)使用:它直接把规则交给 async-validator 执行,
|
|
480
|
+
* 不经过 TDesign 的 `validator(val)` 调用约定,因此不能套 `adaptFormValidator`。
|
|
481
|
+
*/
|
|
482
|
+
export const getNativeRules = function (
|
|
483
|
+
mForm: FormState | undefined,
|
|
484
|
+
r: Rule[] | Rule = [],
|
|
485
|
+
props: any,
|
|
486
|
+
typeMatchValid?: MaybeRef<boolean>,
|
|
487
|
+
) {
|
|
488
|
+
return buildRules(mForm, r, props, typeMatchValid);
|
|
289
489
|
};
|
|
290
490
|
|
|
291
491
|
export const initValue = async (
|