@tmagic/form 1.8.0-manmanyu.8 → 1.8.0
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,561 @@
|
|
|
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 { FormConfig, FormItemConfig, FormState, FormValue, Rule } from '../schema';
|
|
22
|
+
|
|
23
|
+
import { getFieldInnerConfig } from './fieldInnerConfig';
|
|
24
|
+
import { getFieldMountValueEffect, isLeafFieldType } from './fieldValueEffects';
|
|
25
|
+
import {
|
|
26
|
+
appendProp,
|
|
27
|
+
display as displayFunction,
|
|
28
|
+
filterFunction,
|
|
29
|
+
getItemProp,
|
|
30
|
+
getNativeRules,
|
|
31
|
+
isValidName,
|
|
32
|
+
resolveItemType,
|
|
33
|
+
} from './form';
|
|
34
|
+
import {
|
|
35
|
+
getGroupListRowConfig,
|
|
36
|
+
isGroupListType,
|
|
37
|
+
isTableColumnRendered,
|
|
38
|
+
makeTableColumnConfig,
|
|
39
|
+
toGroupListConfig,
|
|
40
|
+
toTableConfig,
|
|
41
|
+
} from './tableGroupList';
|
|
42
|
+
|
|
43
|
+
// #region CollectedField
|
|
44
|
+
/** 一个参与校验的字段:与渲染式校验中「一个带 rules 的 TMagicFormItem」一一对应 */
|
|
45
|
+
export interface CollectedField {
|
|
46
|
+
/** 字段的完整 prop 路径(从表单根 values 起算,与 FormItem 的 prop 一致) */
|
|
47
|
+
prop: string;
|
|
48
|
+
/** 经 `getNativeRules` 处理后的规则(validator 为 async-validator 原生签名) */
|
|
49
|
+
rules: Rule[];
|
|
50
|
+
/** 字段配置 */
|
|
51
|
+
config: FormItemConfig;
|
|
52
|
+
/** 字段所在层级的 model 切片 */
|
|
53
|
+
model: FormValue;
|
|
54
|
+
}
|
|
55
|
+
// #endregion CollectedField
|
|
56
|
+
|
|
57
|
+
/** 已登记的 innerConfig 回调自身抛错时抛出(机制故障,不是漏登记) */
|
|
58
|
+
export class FieldInnerConfigError extends Error {
|
|
59
|
+
readonly code = 'FIELD_INNER_CONFIG';
|
|
60
|
+
readonly type: string;
|
|
61
|
+
readonly prop: string;
|
|
62
|
+
|
|
63
|
+
constructor(type: string, prop: string, cause: unknown) {
|
|
64
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
65
|
+
super(`[MForm] innerConfig for "${type}" at "${prop}" failed: ${reason}`);
|
|
66
|
+
this.name = 'FieldInnerConfigError';
|
|
67
|
+
this.type = type;
|
|
68
|
+
this.prop = prop;
|
|
69
|
+
if (cause instanceof Error) {
|
|
70
|
+
(this as Error & { cause?: unknown }).cause = cause;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const isFieldInnerConfigError = (error: unknown): error is FieldInnerConfigError =>
|
|
76
|
+
error instanceof FieldInnerConfigError ||
|
|
77
|
+
(typeof error === 'object' && error !== null && (error as { code?: string }).code === 'FIELD_INNER_CONFIG');
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 遍历模式。同一套遍历规则服务两个用途,避免两条链路各写一份而产生偏差:
|
|
81
|
+
*
|
|
82
|
+
* - `collect`:收集带 rules 的字段,按 `display` 过滤(对应「渲染出来的 FormItem 集合」);
|
|
83
|
+
* - `effects`:执行字段登记的值初始化写入,不收集字段,且不看 `display`
|
|
84
|
+
* (`display: false` 的字段同样要规整;`type: 'hidden'` 在该节点停止,不往下分派)。
|
|
85
|
+
*/
|
|
86
|
+
type WalkMode = 'collect' | 'effects';
|
|
87
|
+
|
|
88
|
+
interface WalkContext {
|
|
89
|
+
mForm: FormState | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* 是否自动给字段注入 typeMatch 规则,对应 MForm 的 `typeMatchValid`。
|
|
92
|
+
*
|
|
93
|
+
* 只在 `collect` 模式下有用:`addField` 把它交给 `getNativeRules`。为 true 且字段
|
|
94
|
+
* rules 尚未显式声明 `typeMatch`(true / false)时,自动补一条 `{ typeMatch: true }`,
|
|
95
|
+
* 按字段 `type` 校验当前值形态是否合法(空值放行,必填仍靠 `required`)。
|
|
96
|
+
* 单字段可在 rules 里写 `typeMatch: false` 关闭自动注入。
|
|
97
|
+
*
|
|
98
|
+
* `effects` 模式不收集规则,无需传入。
|
|
99
|
+
*/
|
|
100
|
+
typeMatchValid?: boolean;
|
|
101
|
+
fields: CollectedField[];
|
|
102
|
+
/** 本次遍历处理的表单值根对象(对比模式下为 lastValues 那一份) */
|
|
103
|
+
values: FormValue;
|
|
104
|
+
mode: WalkMode;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `effects` 模式下遍历全部配置,不受 display / 折叠状态影响 */
|
|
108
|
+
const ignoresDisplay = (ctx: WalkContext): boolean => ctx.mode === 'effects';
|
|
109
|
+
|
|
110
|
+
interface WalkNode {
|
|
111
|
+
config: FormItemConfig;
|
|
112
|
+
/** 所在层级的 model 切片(对应 Container 的 `props.model`) */
|
|
113
|
+
model: FormValue;
|
|
114
|
+
/** 父级 prop(对应 Container 的 `props.prop`) */
|
|
115
|
+
prop: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type ContainerWalker = (_ctx: WalkContext, _node: WalkNode, _itemProp: string) => void;
|
|
119
|
+
|
|
120
|
+
const extraContainerWalkers = new Map<string, ContainerWalker>();
|
|
121
|
+
const builtInContainerWalkers = new Map<string, ContainerWalker>();
|
|
122
|
+
|
|
123
|
+
export const registerContainerWalker = (type: string, walker: ContainerWalker, builtIn = false): void => {
|
|
124
|
+
if (typeof type !== 'string' || !type || typeof walker !== 'function') return;
|
|
125
|
+
const key = toLine(type);
|
|
126
|
+
if (builtIn) {
|
|
127
|
+
builtInContainerWalkers.set(key, walker);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
extraContainerWalkers.set(key, walker);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const getContainerWalker = (type: string): ContainerWalker | undefined => {
|
|
134
|
+
const key = toLine(type);
|
|
135
|
+
return extraContainerWalkers.get(key) ?? builtInContainerWalkers.get(key);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const deleteContainerWalker = (type: string): boolean => extraContainerWalkers.delete(toLine(type));
|
|
139
|
+
|
|
140
|
+
export const clearContainerWalkers = (): void => extraContainerWalkers.clear();
|
|
141
|
+
|
|
142
|
+
const getItems = (config: any): FormItemConfig[] | undefined => config?.items;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `resolveItemType` 的静态版本:不求值函数型 `type`,由调用方按「未知」处理。
|
|
146
|
+
*
|
|
147
|
+
* 两者必须保持一致,否则预扫描判定的 type 与实际遍历的不同,会漏执行 effect。
|
|
148
|
+
*/
|
|
149
|
+
const staticItemType = (config: any): string => {
|
|
150
|
+
const type = 'type' in config ? config.type : '';
|
|
151
|
+
// form / container 都表示「仅嵌套,不渲染字段」
|
|
152
|
+
if (type === 'form' || type === 'container') return '';
|
|
153
|
+
return `${type || ''}`.replace(/([A-Z])/g, '-$1').toLowerCase() || (config.items ? '' : 'text');
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 静态预判一份配置子树是否可能触发值初始化写入。
|
|
158
|
+
*
|
|
159
|
+
* 只有「确定不会」时才返回 false。函数型 `type`、`itemsFunction`、业务登记的容器遍历器、
|
|
160
|
+
* 登记了 innerConfig 的复合字段,其运行期结构静态看不出来,一律按可能触发处理。
|
|
161
|
+
*
|
|
162
|
+
* 只用在同一份 `items` 会被逐行 / 逐标签页重复展开的地方(table、group-list、dynamic tab):
|
|
163
|
+
* 预判成本是 O(子项数),跳过省下的是 O(行数 × 子项数),无 effect 的多行表格能省一个量级。
|
|
164
|
+
* 其余位置不做预判——扫描与遍历的单节点成本相当,扫完再遍历只会更慢。
|
|
165
|
+
*
|
|
166
|
+
* 只看 `items`:table / group-list 只遍历当前形态的子项,另一形态的
|
|
167
|
+
* `tableItems` / `groupItems` 不参与遍历,判定范围与遍历保持一致。
|
|
168
|
+
*/
|
|
169
|
+
const mayRunEffects = (config: any): boolean => {
|
|
170
|
+
if (!config) return false;
|
|
171
|
+
if (typeof config.type === 'function' || typeof config.itemsFunction === 'function') return true;
|
|
172
|
+
|
|
173
|
+
const type = staticItemType(config);
|
|
174
|
+
|
|
175
|
+
// walkNode 对 hidden 只收集规则,不往下分派
|
|
176
|
+
if (type === 'hidden') return false;
|
|
177
|
+
|
|
178
|
+
if (type) {
|
|
179
|
+
const key = toLine(type);
|
|
180
|
+
// 业务登记的容器遍历路径未知
|
|
181
|
+
if (extraContainerWalkers.has(key)) return true;
|
|
182
|
+
|
|
183
|
+
if (!builtInContainerWalkers.has(key)) {
|
|
184
|
+
if (getFieldInnerConfig(type) || getFieldMountValueEffect(type)) return true;
|
|
185
|
+
// 叶子字段没有子树,dispatchByType 到此为止
|
|
186
|
+
if (isLeafFieldType(type)) return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return itemsMayRunEffects(config.items);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const itemsMayRunEffects = (items: any): boolean =>
|
|
194
|
+
Array.isArray(items) && items.some((item: any) => mayRunEffects(item));
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 复刻 `Container.vue` 的 `display`。
|
|
198
|
+
*
|
|
199
|
+
* 与渲染的唯一差异:`display: 'expand'`(「展开更多配置」按钮背后的字段)在此视为可见。
|
|
200
|
+
* 「是否已展开」纯粹是交互状态,不改变字段是否属于这份配置,无渲染校验按配置声明的范围校验。
|
|
201
|
+
*/
|
|
202
|
+
const resolveDisplay = (ctx: WalkContext, config: any, nodeProps: any): boolean => {
|
|
203
|
+
if (ignoresDisplay(ctx)) return true;
|
|
204
|
+
|
|
205
|
+
const value = displayFunction(ctx.mForm, config?.display, nodeProps);
|
|
206
|
+
if (value === 'expand') return true;
|
|
207
|
+
return Boolean(value);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const addField = (ctx: WalkContext, node: WalkNode, itemProp: string, nodeProps: any): void => {
|
|
211
|
+
if (ctx.mode !== 'collect') return;
|
|
212
|
+
|
|
213
|
+
const rules = getNativeRules(ctx.mForm, (node.config as any).rules, nodeProps, ctx.typeMatchValid) as Rule[];
|
|
214
|
+
if (!rules.length) return;
|
|
215
|
+
|
|
216
|
+
ctx.fields.push({ prop: itemProp, rules, config: node.config, model: node.model });
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const walkChildren = (ctx: WalkContext, items: FormItemConfig[] | undefined, model: FormValue, prop: string): void => {
|
|
220
|
+
if (!Array.isArray(items)) return;
|
|
221
|
+
for (const item of items) {
|
|
222
|
+
if (!item) continue;
|
|
223
|
+
walkNode(ctx, { config: item, model, prop });
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const getContainerScope = (node: WalkNode) => {
|
|
228
|
+
const { config, model } = node;
|
|
229
|
+
const name = (config as any).name || '';
|
|
230
|
+
return {
|
|
231
|
+
config,
|
|
232
|
+
model,
|
|
233
|
+
name,
|
|
234
|
+
items: getItems(config),
|
|
235
|
+
// 容器组件收到的是 Container 自身的 model(父级切片),再由组件内部按 name 下钻
|
|
236
|
+
childModel: (name ? model?.[name] : model) as FormValue,
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** containers/Tabs.vue */
|
|
241
|
+
export const expandTab = (ctx: WalkContext, node: WalkNode, itemProp: string): void => {
|
|
242
|
+
const { config, model, name, items, childModel } = getContainerScope(node);
|
|
243
|
+
const tabsProps = { model, config, prop: itemProp };
|
|
244
|
+
if ((config as any).dynamic) {
|
|
245
|
+
if (!name) return;
|
|
246
|
+
const tabs = model?.[name] || [];
|
|
247
|
+
if (!tabs.length) return;
|
|
248
|
+
// 每个标签页展开同一份 items,逐页展开前先按 items 预判一次
|
|
249
|
+
if (ctx.mode === 'effects' && !itemsMayRunEffects(items)) return;
|
|
250
|
+
tabs.forEach((_tab: any, tabIndex: number) => {
|
|
251
|
+
walkChildren(ctx, items, childModel?.[tabIndex], appendProp(itemProp, tabIndex));
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const tabs = ignoresDisplay(ctx)
|
|
257
|
+
? items || []
|
|
258
|
+
: (items || []).filter((item: any) => displayFunction(ctx.mForm, item?.display, tabsProps));
|
|
259
|
+
for (const tab of tabs) {
|
|
260
|
+
const tabName = (tab as any).name;
|
|
261
|
+
// tab.lazy 只影响渲染时机,不影响该标签页是否属于这份配置,无渲染校验一律遍历
|
|
262
|
+
walkChildren(
|
|
263
|
+
ctx,
|
|
264
|
+
getItems(tab),
|
|
265
|
+
tabName ? childModel?.[tabName] : childModel,
|
|
266
|
+
tabName ? appendProp(itemProp, tabName) : itemProp,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/** containers/Row.vue → Col.vue:col 用 v-show 控制显隐,始终渲染 */
|
|
272
|
+
export const expandRow = (ctx: WalkContext, node: WalkNode, itemProp: string): void => {
|
|
273
|
+
const { items, childModel } = getContainerScope(node);
|
|
274
|
+
walkChildren(ctx, items, childModel, itemProp);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/** containers/Fieldset.vue */
|
|
278
|
+
export const expandFieldset = (ctx: WalkContext, node: WalkNode, itemProp: string): void => {
|
|
279
|
+
const { config, items, childModel } = getContainerScope(node);
|
|
280
|
+
if (!childModel) return;
|
|
281
|
+
const { checkbox } = config as any;
|
|
282
|
+
const checkboxName = typeof checkbox === 'object' && typeof checkbox.name === 'string' ? checkbox.name : 'value';
|
|
283
|
+
const checkboxTrueValue =
|
|
284
|
+
typeof checkbox === 'object' && typeof checkbox.trueValue !== 'undefined' ? checkbox.trueValue : 1;
|
|
285
|
+
// 勾选框关闭时整个 fieldset 的子项不渲染,语义上等于「该段配置未启用」,不参与校验
|
|
286
|
+
if (!ignoresDisplay(ctx) && (config as any).expand && childModel?.[checkboxName] !== checkboxTrueValue) return;
|
|
287
|
+
walkChildren(ctx, items, childModel, itemProp);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/** containers/Panel.vue:折叠仅 display:none,子项照常渲染 */
|
|
291
|
+
export const expandPanel = (ctx: WalkContext, node: WalkNode, itemProp: string): void => {
|
|
292
|
+
const { items, childModel } = getContainerScope(node);
|
|
293
|
+
if (!items?.length) return;
|
|
294
|
+
walkChildren(ctx, items, childModel, itemProp);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/** containers/Step.vue:非当前步仅 v-show 隐藏,子项照常渲染;prop 基准被重置为 step.name */
|
|
298
|
+
export const expandStep = (ctx: WalkContext, node: WalkNode, _itemProp: string): void => {
|
|
299
|
+
const { model, items } = getContainerScope(node);
|
|
300
|
+
for (const step of items || []) {
|
|
301
|
+
const stepName = (step as any)?.name;
|
|
302
|
+
walkChildren(ctx, getItems(step), stepName ? model?.[stepName] : model, `${stepName}`);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 遍历 table / group-list。
|
|
308
|
+
*
|
|
309
|
+
* `TableGroupList.vue` 按原始 `config.type` 决定初始展示形态(table 或 groupList),
|
|
310
|
+
* 两种形态的子项结构不同,这里分别复刻(形态间的配置派生复用 `utils/tableGroupList`,
|
|
311
|
+
* 与渲染式实现同源):
|
|
312
|
+
* - table:逐列(跳过 hidden 列与 display 为假的列)× 逐行渲染 Container,行 prop 为 `${prop}.${index}`;
|
|
313
|
+
* - groupList:逐行渲染一个 row 容器,包含 `items` 全部字段。
|
|
314
|
+
*
|
|
315
|
+
* 与渲染的差异:不做分页截断(table)、不跳过折叠行(groupList)。二者都是列表的视图状态,
|
|
316
|
+
* 与「这些行的值是否合法」无关,无渲染校验覆盖全部行。
|
|
317
|
+
*/
|
|
318
|
+
export const expandTableGroupList = (ctx: WalkContext, node: WalkNode, itemProp: string): void => {
|
|
319
|
+
const { config, model } = node;
|
|
320
|
+
const name = (config as any).name || '';
|
|
321
|
+
const rows = model?.[name];
|
|
322
|
+
if (!Array.isArray(rows) || !rows.length) return;
|
|
323
|
+
|
|
324
|
+
if (isGroupListType((config as any).type)) {
|
|
325
|
+
const groupListConfig = toGroupListConfig(config as any);
|
|
326
|
+
|
|
327
|
+
// 行数是配置项数的倍数,逐行展开前先按列配置预判一次,避免整表白跑
|
|
328
|
+
if (ctx.mode === 'effects' && !itemsMayRunEffects(groupListConfig.items)) return;
|
|
329
|
+
|
|
330
|
+
rows.forEach((row, index) => {
|
|
331
|
+
walkNode(ctx, {
|
|
332
|
+
config: getGroupListRowConfig(groupListConfig, index, ctx.mForm?.keyProp) as FormItemConfig,
|
|
333
|
+
model: row,
|
|
334
|
+
prop: appendProp(itemProp, index),
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const tableItems = toTableConfig(config as any).items;
|
|
341
|
+
if (!Array.isArray(tableItems)) return;
|
|
342
|
+
|
|
343
|
+
if (ctx.mode === 'effects' && !itemsMayRunEffects(tableItems)) return;
|
|
344
|
+
|
|
345
|
+
// 列的 display 在 Table 层用「表格自身的 props」求值,随后 makeTableColumnConfig 会删掉 display
|
|
346
|
+
const tableProps = { model, config, prop: itemProp };
|
|
347
|
+
const evalDisplay = (display: any) => displayFunction(ctx.mForm, display, tableProps);
|
|
348
|
+
const isRendered = (column: any) => ignoresDisplay(ctx) || isTableColumnRendered(column, evalDisplay);
|
|
349
|
+
|
|
350
|
+
rows.forEach((row, index) => {
|
|
351
|
+
for (const column of tableItems) {
|
|
352
|
+
if (!column || !isRendered(column)) continue;
|
|
353
|
+
|
|
354
|
+
walkNode(ctx, {
|
|
355
|
+
config: makeTableColumnConfig(column, row) as FormItemConfig,
|
|
356
|
+
model: row,
|
|
357
|
+
prop: appendProp(itemProp, index),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 执行某个叶子字段登记的值初始化写入。
|
|
365
|
+
*
|
|
366
|
+
* 跑在表单初始化路径上,单个字段的 effect 抛错不应该让整张表单渲染不出来,因此只记录并继续。
|
|
367
|
+
*/
|
|
368
|
+
const runMountValueEffect = (ctx: WalkContext, type: string, node: WalkNode, itemProp: string): void => {
|
|
369
|
+
const effect = getFieldMountValueEffect(type);
|
|
370
|
+
if (!effect) return;
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
effect({ config: node.config, model: node.model, prop: itemProp, mForm: ctx.mForm, values: ctx.values });
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.error(`[MForm] mount value effect for "${type}" at "${itemProp}" failed:`, e);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 遍历已登记 innerConfig 的复合字段。
|
|
381
|
+
*
|
|
382
|
+
* `collect` 模式下回调抛错视为机制故障,包装成 `FieldInnerConfigError` 抛出,由校验流程暴露。
|
|
383
|
+
* `effects` 模式跑在表单初始化路径上,抛出会让整张表单渲染不出来,因此只记录并跳过该子树。
|
|
384
|
+
*/
|
|
385
|
+
const walkInnerConfig = (ctx: WalkContext, type: string, node: WalkNode, itemProp: string): boolean => {
|
|
386
|
+
const resolve = getFieldInnerConfig(type);
|
|
387
|
+
if (!resolve) return false;
|
|
388
|
+
|
|
389
|
+
let result;
|
|
390
|
+
try {
|
|
391
|
+
result = resolve({
|
|
392
|
+
config: node.config,
|
|
393
|
+
model: node.model,
|
|
394
|
+
prop: itemProp,
|
|
395
|
+
parentProp: node.prop,
|
|
396
|
+
mForm: ctx.mForm,
|
|
397
|
+
});
|
|
398
|
+
} catch (e) {
|
|
399
|
+
if (ctx.mode === 'collect') {
|
|
400
|
+
throw new FieldInnerConfigError(type, itemProp, e);
|
|
401
|
+
}
|
|
402
|
+
console.error(new FieldInnerConfigError(type, itemProp, e));
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!result) return true;
|
|
407
|
+
|
|
408
|
+
const innerModel = result.model ?? node.model;
|
|
409
|
+
const innerProp = result.prop ?? itemProp;
|
|
410
|
+
const innerConfig = Array.isArray(result.config) ? result.config : [result.config];
|
|
411
|
+
walkChildren(ctx, innerConfig, innerModel, innerProp);
|
|
412
|
+
return true;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 遍历一个 Container 节点,分支顺序与 `Container.vue` 的模板保持一致。
|
|
417
|
+
*
|
|
418
|
+
* 对比模式(`isCompare`)在此被忽略:它只改变同一个 prop 渲染几份 FormItem,
|
|
419
|
+
* 规则与取值完全相同,对校验结果没有影响。
|
|
420
|
+
*/
|
|
421
|
+
const walkNode = (ctx: WalkContext, node: WalkNode): void => {
|
|
422
|
+
const { config, prop } = node;
|
|
423
|
+
const model = node.model ?? {};
|
|
424
|
+
// 与 Container 的 props 对齐:`prop` 是父级 prop,供 display / rules 的上下文使用
|
|
425
|
+
const nodeProps = { model, config, prop };
|
|
426
|
+
|
|
427
|
+
const name = (config as any).name || '';
|
|
428
|
+
const items = getItems(config);
|
|
429
|
+
const type = resolveItemType(ctx.mForm, config, nodeProps);
|
|
430
|
+
const itemProp = getItemProp(prop, name);
|
|
431
|
+
const text = filterFunction(ctx.mForm, (config as any).text, nodeProps);
|
|
432
|
+
|
|
433
|
+
// hidden 不看 display,始终渲染一个隐藏的 FormItem
|
|
434
|
+
if (type === 'hidden') {
|
|
435
|
+
addField(ctx, { ...node, model }, itemProp, nodeProps);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const display = resolveDisplay(ctx, config, nodeProps);
|
|
440
|
+
|
|
441
|
+
// 容器分支:不渲染自身的 FormItem,只渲染容器组件。
|
|
442
|
+
// 容器自身的 rules 只有在带 text、会包一层 FormItem 时才会生效(见下一分支),与 Container.vue 一致。
|
|
443
|
+
if (items && !text && type && display) {
|
|
444
|
+
dispatchByType(ctx, type, { ...node, model }, itemProp);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// FormItem 分支:只要会包 FormItem,有 rules 就会校验,不要求 type 已登记为叶子。
|
|
449
|
+
if (type && display) {
|
|
450
|
+
addField(ctx, { ...node, model }, itemProp, nodeProps);
|
|
451
|
+
dispatchByType(ctx, type, { ...node, model }, itemProp);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 无 type 的纯嵌套配置:直接递归子项
|
|
456
|
+
if (items && display) {
|
|
457
|
+
const childModel = isValidName(name) ? model[name] : model;
|
|
458
|
+
if (!childModel) return;
|
|
459
|
+
walkChildren(ctx, items, childModel, itemProp);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 按 type 分派:已登记 walk 的容器 → 按容器模板遍历;
|
|
465
|
+
* `effects` 模式下先执行本字段的 effect(可与 innerConfig 并存,如 `code-select` 先归一化再下钻);
|
|
466
|
+
* 登记了 innerConfig 的复合字段 → 遍历其内部配置;叶子字段无子树。
|
|
467
|
+
*
|
|
468
|
+
* 未登记的 type:配置里若有 `items` 则按普通容器下钻;否则视为没有嵌套表单项。
|
|
469
|
+
* 自身 rules 已在 FormItem 分支收集,有 rules 就会校验。
|
|
470
|
+
*/
|
|
471
|
+
const dispatchByType = (ctx: WalkContext, type: string, node: WalkNode, itemProp: string): void => {
|
|
472
|
+
const walkContainer = getContainerWalker(type);
|
|
473
|
+
if (walkContainer) {
|
|
474
|
+
walkContainer(ctx, node, itemProp);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (ctx.mode === 'effects') {
|
|
479
|
+
runMountValueEffect(ctx, type, node, itemProp);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (walkInnerConfig(ctx, type, node, itemProp)) return;
|
|
483
|
+
|
|
484
|
+
if (isLeafFieldType(type)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const items = getItems(node.config);
|
|
489
|
+
if (items?.length) {
|
|
490
|
+
const { childModel } = getContainerScope(node);
|
|
491
|
+
walkChildren(ctx, items, childModel, itemProp);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 纯逻辑收集「一份 config + 一份 values」中所有参与校验的字段。
|
|
497
|
+
*
|
|
498
|
+
* 遍历规则与 `Container.vue` 及各容器组件的模板一一对应,因此产出的 prop / rules
|
|
499
|
+
* 与渲染式校验(挂载 MForm 后调用 `validate()`)等价,无需任何 DOM 或组件实例。
|
|
500
|
+
*
|
|
501
|
+
* 本函数只读不写。字段的值初始化副作用由 `applyMountValueEffects` 负责,
|
|
502
|
+
* 调用方需在初始化表单值之后、收集字段之前执行一次(`validateValues` 已经这么做)。
|
|
503
|
+
*
|
|
504
|
+
* @param mForm - 表单状态;无渲染时由 `createHeadlessFormState` 构造
|
|
505
|
+
* @param config - 表单配置
|
|
506
|
+
* @param values - 表单值
|
|
507
|
+
* @param [typeMatchValid] - 是否自动注入 typeMatch 规则;为 true 时,未显式声明
|
|
508
|
+
* `typeMatch` 的字段会补上 `{ typeMatch: true }`,按字段 type 校验值形态
|
|
509
|
+
* @returns 带规则的字段列表
|
|
510
|
+
*/
|
|
511
|
+
export const collectValidatableFields = (
|
|
512
|
+
mForm: FormState | undefined,
|
|
513
|
+
config: FormConfig,
|
|
514
|
+
values: FormValue,
|
|
515
|
+
typeMatchValid?: boolean,
|
|
516
|
+
): CollectedField[] => {
|
|
517
|
+
const ctx: WalkContext = {
|
|
518
|
+
mForm,
|
|
519
|
+
typeMatchValid,
|
|
520
|
+
fields: [],
|
|
521
|
+
values,
|
|
522
|
+
mode: 'collect',
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
if (Array.isArray(config)) {
|
|
526
|
+
walkChildren(ctx, config as FormItemConfig[], values, '');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return ctx.fields;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 执行「一份 config + 一份 values」中所有叶子字段登记的值初始化写入,就地改写 `values`。
|
|
534
|
+
*
|
|
535
|
+
* 渲染与无渲染两条链路共用的唯一执行点:字段组件自身不再在 setup 里改写 model,
|
|
536
|
+
* 由持有完整表单值的一方(`Form.vue` / `validateValues`)在表单值初始化完成、
|
|
537
|
+
* 且已挂到 `mForm.values` 之后调用一次。这样 effect 与动态 `type` 回调读到的
|
|
538
|
+
* `formValue` 就是最新值,而不是上一轮的旧值。
|
|
539
|
+
*
|
|
540
|
+
* 与 `collectValidatableFields` 的差异:不看 `display`(`display: false` 的字段也会规整)。
|
|
541
|
+
* `type: 'hidden'` 会在该节点停止遍历,内部字段不执行 effect。
|
|
542
|
+
*
|
|
543
|
+
* `prop` 与 `values` 都以传入的 `values` 为根,因此也可以用于 tab / table 新增行这类
|
|
544
|
+
* 「先构造一份子树的值,再挂到表单上」的场景:传入子树配置与该行的值即可。
|
|
545
|
+
*
|
|
546
|
+
* @param mForm - 表单状态;无渲染时由 `createHeadlessFormState` 构造
|
|
547
|
+
* @param config - 表单配置
|
|
548
|
+
* @param values - 表单值(会被就地修改)
|
|
549
|
+
*/
|
|
550
|
+
export const applyMountValueEffects = (mForm: FormState | undefined, config: FormConfig, values: FormValue): void => {
|
|
551
|
+
if (!Array.isArray(config)) return;
|
|
552
|
+
|
|
553
|
+
const ctx: WalkContext = {
|
|
554
|
+
mForm,
|
|
555
|
+
fields: [],
|
|
556
|
+
values,
|
|
557
|
+
mode: 'effects',
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
walkChildren(ctx, config as FormItemConfig[], values, '');
|
|
561
|
+
};
|
package/src/utils/config.ts
CHANGED
|
@@ -16,27 +16,19 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import
|
|
19
|
+
import { ref } from 'vue';
|
|
20
20
|
|
|
21
21
|
let $MAGIC_FORM = {} as any;
|
|
22
|
+
// 用 ref 持有 flat 全局开关:const 引用本身不可变,符合 `import/no-mutable-exports`;
|
|
23
|
+
// 通过 `.value` 改值仍保持模块级响应式语义,与 editor/plugin.ts、design/index.ts 里
|
|
24
|
+
// 的同名变量写法对齐。
|
|
25
|
+
const isGlobalFlat = ref(false);
|
|
22
26
|
|
|
23
27
|
const setConfig = (option: any): void => {
|
|
24
28
|
$MAGIC_FORM = option;
|
|
29
|
+
isGlobalFlat.value = option.flat ?? false;
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
const getConfig = <T = unknown>(key: string): T => $MAGIC_FORM[key];
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const registerField = (tagName: string, component: Component): void => {
|
|
32
|
-
if (fieldRegistry.has(tagName)) {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
fieldRegistry.set(tagName, component);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const getField = (tagName: string): Component | undefined => fieldRegistry.get(tagName);
|
|
39
|
-
|
|
40
|
-
const deleteField = (tagName: string): boolean => fieldRegistry.delete(tagName);
|
|
41
|
-
|
|
42
|
-
export { deleteField, getConfig, getField, registerField, setConfig };
|
|
34
|
+
export { getConfig, isGlobalFlat, setConfig };
|