@qin-ui/antd-vue-pro 2.5.2 → 2.5.3
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/api.json +2 -2
- package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -1
- package/es/components/form/components/BaseFormItem/index.vue.d.ts +59 -11
- package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -1
- package/es/components/form/components/BaseFormItems/index.vue.d.ts +16 -0
- package/es/components/form/components/BaseFormItems/index.vue.d.ts.map +1 -0
- package/es/components/form/components/index.d.ts +6 -6
- package/es/components/form/components/index.d.ts.map +1 -1
- package/es/components/form/hooks/useGroupFieldAttrs.d.ts +13 -3
- package/es/components/form/hooks/useGroupFieldAttrs.d.ts.map +1 -1
- package/es/components/form/utils/fieldKey.d.ts +12 -0
- package/es/components/form/utils/fieldKey.d.ts.map +1 -1
- package/es/index.js +297 -158
- package/es/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/components/BaseField/index.vue +30 -9
- package/src/components/form/components/BaseFormItem/index.vue +153 -186
- package/src/components/form/components/BaseFormItems/index.vue +186 -0
- package/src/components/form/components/ProForm/index.vue +5 -5
- package/src/components/form/components/index.ts +6 -6
- package/src/components/form/hooks/useGroupFieldAttrs.ts +114 -88
- package/src/components/form/utils/fieldKey.ts +63 -0
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
computed,
|
|
34
34
|
inject,
|
|
35
35
|
mergeProps,
|
|
36
|
+
onBeforeUnmount,
|
|
36
37
|
ref,
|
|
37
38
|
toValue,
|
|
38
39
|
} from 'vue';
|
|
@@ -63,6 +64,9 @@ const cloneDeepIfObject = (val: unknown) =>
|
|
|
63
64
|
// 原始值直接传递,引用值克隆快照
|
|
64
65
|
typeof val === 'object' && val !== null ? cloneDeep(val) : val;
|
|
65
66
|
|
|
67
|
+
/** SameValueZero 同值判定(`0` / `-0`、`NaN` / `NaN` 视为同值) */
|
|
68
|
+
const isSameValue = (a: unknown, b: unknown) => a === b || (a !== a && b !== b);
|
|
69
|
+
|
|
66
70
|
defineOptions({ name: 'BaseField', inheritAttrs: false });
|
|
67
71
|
|
|
68
72
|
type Props = {
|
|
@@ -99,10 +103,25 @@ useProvideFormItemContext({
|
|
|
99
103
|
clearValidate: formItemContext.clearValidate,
|
|
100
104
|
});
|
|
101
105
|
|
|
102
|
-
|
|
106
|
+
/*
|
|
107
|
+
* 值变更通知按微任务合并:同一 tick 多次写入只回传最后一次;
|
|
108
|
+
* 组件卸载时未发送的通知不再发出
|
|
109
|
+
*/
|
|
110
|
+
let pendingChange = false;
|
|
111
|
+
let pendingChangeValue: unknown;
|
|
103
112
|
const triggerFormItemChange = (value: unknown) => {
|
|
104
|
-
|
|
113
|
+
pendingChangeValue = value;
|
|
114
|
+
if (pendingChange) return;
|
|
115
|
+
pendingChange = true;
|
|
116
|
+
queueMicrotask(() => {
|
|
117
|
+
if (!pendingChange) return;
|
|
118
|
+
pendingChange = false;
|
|
119
|
+
emit('field-value-change', pendingChangeValue);
|
|
120
|
+
});
|
|
105
121
|
};
|
|
122
|
+
onBeforeUnmount(() => {
|
|
123
|
+
pendingChange = false;
|
|
124
|
+
});
|
|
106
125
|
|
|
107
126
|
const attrs: any = useReactiveAttrs();
|
|
108
127
|
|
|
@@ -148,17 +167,19 @@ const value = computed({
|
|
|
148
167
|
typeof valueFormatter?.set === 'function'
|
|
149
168
|
? valueFormatter.set
|
|
150
169
|
: undefined;
|
|
170
|
+
const oldVal = form.getFormData?.(props.path);
|
|
151
171
|
|
|
152
172
|
if (setFormatter) {
|
|
153
173
|
// 旧值仅在 set 转换函数声明双形参时注入
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
newVal = setFormatter(
|
|
175
|
+
val,
|
|
176
|
+
setFormatter.length > 1 ? cloneDeepIfObject(oldVal) : undefined
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (!isSameValue(newVal, oldVal)) {
|
|
180
|
+
form.setFormData?.(props.path, newVal);
|
|
181
|
+
triggerFormItemChange(newVal);
|
|
159
182
|
}
|
|
160
|
-
form.setFormData?.(props.path, newVal);
|
|
161
|
-
triggerFormItemChange(newVal);
|
|
162
183
|
// 字段配置/attrs 传入的 onUpdate:* 监听器与内部写值组合回放,参数为组件原始 emit 值
|
|
163
184
|
const { onUpdateValue } = groupedAttrs.value;
|
|
164
185
|
if (typeof onUpdateValue === 'function') {
|
|
@@ -1,211 +1,178 @@
|
|
|
1
|
-
<script lang="ts"
|
|
1
|
+
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 单字段行渲染组件
|
|
4
4
|
*
|
|
5
5
|
* @component BaseFormItem
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* - FormItem
|
|
11
|
-
* - 组件绑定(通过 BaseField 渲染具体组件)
|
|
12
|
-
* - 插槽透传(label、extra、help 等 FormItem 插槽)
|
|
7
|
+
* 将单个字段配置渲染为栅格项、FormItem 与控件,支持:
|
|
8
|
+
* - 字段属性分流(grid / formItem / component / slots)
|
|
9
|
+
* - 嵌套字段递归渲染(交由 BaseFormItems)
|
|
10
|
+
* - FormItem / 控件实例登记
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 1. 遍历 fields 数组,过滤 hidden 字段
|
|
17
|
-
* 2. 将字段属性分流为 GridItem / FormItem / 组件 / 插槽四类
|
|
18
|
-
* 3. 递归渲染 BaseFormItem(嵌套表单)或 BaseField(输入组件)
|
|
19
|
-
* 4. 渲染后字段对象上可读取 FormItem 与组件实例
|
|
20
|
-
*
|
|
21
|
-
* @param {Fields} [fields] - 字段配置数组
|
|
12
|
+
* @param {Field} field - 字段配置
|
|
22
13
|
* @param {boolean | GridProps} [grid] - 是否启用网格布局
|
|
23
|
-
* @param {boolean} [disabled] -
|
|
14
|
+
* @param {boolean} [disabled] - 上层下发的禁用状态
|
|
15
|
+
* @param {Function} [setFormItemRef] - FormItem 实例登记回调
|
|
16
|
+
* @param {Function} [setComponentRef] - 控件实例登记回调
|
|
24
17
|
*
|
|
25
18
|
* @internal
|
|
26
19
|
*/
|
|
27
|
-
import { getObject, isObjectItem, toPath } from '../../../../shared/core';
|
|
28
|
-
import type { GridProps } from '../../../../shared/ui';
|
|
29
20
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
computed,
|
|
22
|
+
defineComponent,
|
|
23
|
+
h,
|
|
24
|
+
toValue,
|
|
25
|
+
watch,
|
|
26
|
+
type PropType,
|
|
27
|
+
type VNode,
|
|
28
|
+
} from 'vue';
|
|
29
|
+
import { toPath } from '../../../../shared/core';
|
|
30
|
+
import { FormItem, GridItem as AGridItem } from '../../../../shared/ui';
|
|
31
|
+
import type { Field, Grid } from '../../types';
|
|
37
32
|
import BaseField from '../BaseField/index.vue';
|
|
33
|
+
import BaseFormItems from '../BaseFormItems/index.vue';
|
|
38
34
|
import SlotComponent from '../SlotComponent/index.vue';
|
|
39
|
-
import
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
import {
|
|
36
|
+
useGroupedField,
|
|
37
|
+
type GroupedField,
|
|
38
|
+
} from '../../hooks/useGroupFieldAttrs';
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
export default defineComponent({
|
|
41
|
+
name: 'BaseFormItem',
|
|
42
|
+
inheritAttrs: false,
|
|
43
|
+
props: {
|
|
44
|
+
/** 字段配置 */
|
|
45
|
+
field: { type: Object as PropType<Field>, required: true },
|
|
46
|
+
/** 是否启用网格布局 */
|
|
47
|
+
grid: {
|
|
48
|
+
type: [Boolean, Object] as PropType<Grid>,
|
|
49
|
+
default: undefined,
|
|
50
|
+
},
|
|
51
|
+
/** 上层下发的禁用状态 */
|
|
52
|
+
disabled: { type: Boolean, default: undefined },
|
|
53
|
+
/** FormItem 实例登记回调 */
|
|
54
|
+
setFormItemRef: {
|
|
55
|
+
type: Function as PropType<
|
|
56
|
+
(field: Field, el: any, getFormItemProps: () => any) => void
|
|
57
|
+
>,
|
|
58
|
+
default: undefined,
|
|
59
|
+
},
|
|
60
|
+
/** 控件实例登记回调 */
|
|
61
|
+
setComponentRef: {
|
|
62
|
+
type: Function as PropType<(field: Field, el: any) => void>,
|
|
63
|
+
default: undefined,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
setup(props) {
|
|
67
|
+
const grouped = useGroupedField(() => props.field);
|
|
68
|
+
const enableGrid = computed(() => !!props.grid);
|
|
44
69
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
54
|
-
grid: undefined,
|
|
55
|
-
fields: () => [],
|
|
56
|
-
disabled: undefined,
|
|
57
|
-
});
|
|
70
|
+
// 控件级配置身份:字符串组件名 + picker 段;组件对象无 Provider 配置,使用固定值
|
|
71
|
+
const componentKey = computed(() => {
|
|
72
|
+
const field = props.field as any;
|
|
73
|
+
const component = toValue(field.component);
|
|
74
|
+
if (typeof component !== 'string') return 'component-object';
|
|
75
|
+
return `string:${component}:${toValue(field.picker) ?? ''}`;
|
|
76
|
+
});
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
(props.fields ?? []).filter(
|
|
62
|
-
field => isObjectItem(field) && !toValue((field as any).hidden)
|
|
63
|
-
)
|
|
64
|
-
);
|
|
78
|
+
/** 当前字段的宿主 FormItem 实例 */
|
|
79
|
+
let formItemInstance: any;
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
const setItemRef = (el: any) => {
|
|
82
|
+
formItemInstance = el;
|
|
83
|
+
props.setFormItemRef?.(
|
|
84
|
+
props.field,
|
|
85
|
+
el,
|
|
86
|
+
() => grouped.value.formItemProps
|
|
87
|
+
);
|
|
88
|
+
};
|
|
68
89
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
const setFieldRef = (el: any) => {
|
|
91
|
+
props.setComponentRef?.(props.field, el);
|
|
92
|
+
};
|
|
72
93
|
|
|
73
|
-
|
|
94
|
+
// 字段对象变化后,实例访问器归属当前字段
|
|
95
|
+
watch(
|
|
96
|
+
() => props.field,
|
|
97
|
+
(next, prev) => {
|
|
98
|
+
if (!prev || prev === next) return;
|
|
99
|
+
props.setFormItemRef?.(prev, null, () => grouped.value.formItemProps);
|
|
100
|
+
props.setComponentRef?.(prev, null);
|
|
101
|
+
},
|
|
102
|
+
{ flush: 'pre' }
|
|
103
|
+
);
|
|
74
104
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
/*
|
|
106
|
+
* 字段值变更经 BaseField 的 field-value-change 事件回传,由当前字段的 FormItem 实例触发校验;
|
|
107
|
+
* autoLink 为 false 时保持用户自行校验的语义
|
|
108
|
+
*/
|
|
109
|
+
const onFieldValueChange = () => {
|
|
110
|
+
if (grouped.value.formItemProps?.autoLink === false) return;
|
|
111
|
+
formItemInstance?.onFieldChange?.();
|
|
112
|
+
};
|
|
80
113
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
/** 按容器组件逐层包裹当前节点 */
|
|
115
|
+
const withContainer = (
|
|
116
|
+
node: VNode,
|
|
117
|
+
component: any,
|
|
118
|
+
containerProps: Record<string, any>
|
|
119
|
+
): VNode =>
|
|
120
|
+
component ? h(component, containerProps, { default: () => node }) : node;
|
|
87
121
|
|
|
88
|
-
/**
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
value: getter,
|
|
99
|
-
enumerable: false,
|
|
100
|
-
configurable: true,
|
|
101
|
-
writable: true,
|
|
122
|
+
/** 组装 FormItem 的动态插槽与默认插槽 */
|
|
123
|
+
const renderFormItemSlots = (groupedField: GroupedField) => {
|
|
124
|
+
const slots: Record<string, any> = {};
|
|
125
|
+
Object.entries(groupedField.formItemSlots).forEach(([name, slot]) => {
|
|
126
|
+
slots[name] = (scope?: Record<string, any>) =>
|
|
127
|
+
h(SlotComponent as any, {
|
|
128
|
+
path: groupedField.path,
|
|
129
|
+
component: slot,
|
|
130
|
+
...(scope || {}),
|
|
131
|
+
});
|
|
102
132
|
});
|
|
103
|
-
}
|
|
104
|
-
} catch {
|
|
105
|
-
if (!warnedFrozenFields.has(raw)) {
|
|
106
|
-
warnedFrozenFields.add(raw);
|
|
107
|
-
console.warn(
|
|
108
|
-
'[pro-components] 字段配置对象不可写(如被 Object.freeze),getFormItemRef / getComponentRef 等实例访问器挂载失败。'
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
133
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
} else {
|
|
134
|
-
formItemInstanceMap.delete(raw);
|
|
135
|
-
delete raw.getFormItemRef;
|
|
136
|
-
delete raw.getFormItemComputedProps;
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const setComponentRef = (field: any, el: any) => {
|
|
141
|
-
if (!field || typeof field !== 'object') return;
|
|
142
|
-
const raw = toRaw(field);
|
|
143
|
-
if (el) {
|
|
144
|
-
componentInstanceMap.set(raw, el);
|
|
145
|
-
defineFieldAccessors(raw, {
|
|
146
|
-
getComponentRef: () => componentInstanceMap.get(raw)?.getComponentRef(),
|
|
147
|
-
getComponentComputedProps: () =>
|
|
148
|
-
componentInstanceMap.get(raw)?.getComponentComputedProps(),
|
|
149
|
-
});
|
|
150
|
-
} else {
|
|
151
|
-
componentInstanceMap.delete(raw);
|
|
152
|
-
delete raw.getComponentRef;
|
|
153
|
-
delete raw.getComponentComputedProps;
|
|
154
|
-
}
|
|
155
|
-
};
|
|
134
|
+
slots.default = () => {
|
|
135
|
+
const field = props.field as any;
|
|
136
|
+
if (field.fields) {
|
|
137
|
+
return h(BaseFormItems as any, {
|
|
138
|
+
grid: field.grid ?? props.grid,
|
|
139
|
+
fields: field.fields,
|
|
140
|
+
disabled: toValue(field.disabled),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return h(BaseField as any, {
|
|
144
|
+
key: componentKey.value,
|
|
145
|
+
ref: setFieldRef,
|
|
146
|
+
...groupedField.componentProps,
|
|
147
|
+
path: groupedField.path,
|
|
148
|
+
onFieldValueChange,
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
return slots;
|
|
152
|
+
};
|
|
156
153
|
|
|
157
|
-
|
|
154
|
+
return () => {
|
|
155
|
+
const groupedField = grouped.value;
|
|
156
|
+
let node: VNode = h(
|
|
157
|
+
FormItem as any,
|
|
158
|
+
{
|
|
159
|
+
...groupedField.formItemProps,
|
|
160
|
+
ref: setItemRef,
|
|
161
|
+
name: toPath(groupedField.path),
|
|
162
|
+
path: groupedField.path,
|
|
163
|
+
},
|
|
164
|
+
renderFormItemSlots(groupedField)
|
|
165
|
+
);
|
|
166
|
+
node = withContainer(node, groupedField.formItemContainer, {
|
|
167
|
+
path: groupedField.path,
|
|
168
|
+
});
|
|
169
|
+
node = withContainer(
|
|
170
|
+
node,
|
|
171
|
+
enableGrid.value ? AGridItem : undefined,
|
|
172
|
+
groupedField.gridItemProps
|
|
173
|
+
);
|
|
174
|
+
return node;
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
});
|
|
158
178
|
</script>
|
|
159
|
-
|
|
160
|
-
<template>
|
|
161
|
-
<ContainerFragment
|
|
162
|
-
:component="enableGrid ? AGrid : undefined"
|
|
163
|
-
v-bind="computedGridProps"
|
|
164
|
-
>
|
|
165
|
-
<template v-for="item of groupedFields" :key="item.renderKey">
|
|
166
|
-
<ContainerFragment
|
|
167
|
-
:component="enableGrid ? AGridItem : undefined"
|
|
168
|
-
v-bind="item.gridItemProps"
|
|
169
|
-
>
|
|
170
|
-
<ContainerFragment
|
|
171
|
-
:component="item.formItemContainer"
|
|
172
|
-
:path="item.path"
|
|
173
|
-
>
|
|
174
|
-
<FormItem
|
|
175
|
-
v-bind="item.formItemProps"
|
|
176
|
-
:ref="el => setFormItemRef(item.rawField, el, item.formItemProps)"
|
|
177
|
-
:name="toPath(item.path)"
|
|
178
|
-
:path="item.path"
|
|
179
|
-
>
|
|
180
|
-
<template
|
|
181
|
-
v-for="(slot, name) in item.formItemSlots"
|
|
182
|
-
:key="name"
|
|
183
|
-
#[name]="scoped"
|
|
184
|
-
>
|
|
185
|
-
<SlotComponent
|
|
186
|
-
:path="item.path"
|
|
187
|
-
:component="slot"
|
|
188
|
-
v-bind="scoped"
|
|
189
|
-
/>
|
|
190
|
-
</template>
|
|
191
|
-
<template v-if="item.rawField.fields">
|
|
192
|
-
<BaseFormItem
|
|
193
|
-
:grid="(item.rawField as any).grid ?? props.grid"
|
|
194
|
-
:fields="item.rawField.fields"
|
|
195
|
-
:disabled="toValue(item.rawField.disabled)"
|
|
196
|
-
/>
|
|
197
|
-
</template>
|
|
198
|
-
<template v-else>
|
|
199
|
-
<BaseField
|
|
200
|
-
:ref="el => setComponentRef(item.rawField, el)"
|
|
201
|
-
v-bind="item.componentProps"
|
|
202
|
-
:path="item.path"
|
|
203
|
-
@field-value-change="onFieldValueChange(item)"
|
|
204
|
-
/>
|
|
205
|
-
</template>
|
|
206
|
-
</FormItem>
|
|
207
|
-
</ContainerFragment>
|
|
208
|
-
</ContainerFragment>
|
|
209
|
-
</template>
|
|
210
|
-
</ContainerFragment>
|
|
211
|
-
</template>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* 表单字段渲染引擎
|
|
4
|
+
*
|
|
5
|
+
* @component BaseFormItems
|
|
6
|
+
*
|
|
7
|
+
* 负责将字段配置数组渲染为实际的表单项,支持:
|
|
8
|
+
* - 字段分组(嵌套 fields 递归渲染)
|
|
9
|
+
* - 网格布局(grid 属性控制)
|
|
10
|
+
* - FormItem 包装(label、rules、校验等)
|
|
11
|
+
* - 组件绑定(通过 BaseField 渲染具体组件)
|
|
12
|
+
* - 插槽透传(label、extra、help 等 FormItem 插槽)
|
|
13
|
+
*
|
|
14
|
+
* ## 渲染流程
|
|
15
|
+
*
|
|
16
|
+
* 1. 遍历 fields 数组,过滤 hidden 字段
|
|
17
|
+
* 2. 每个字段交由 BaseFormItem 拆分为 GridItem / FormItem / 组件 / 插槽并渲染
|
|
18
|
+
* 3. 嵌套 fields 递归渲染下一层 BaseFormItems
|
|
19
|
+
* 4. 渲染后字段对象上可读取 FormItem 与组件实例
|
|
20
|
+
*
|
|
21
|
+
* @param {Fields} [fields] - 字段配置数组
|
|
22
|
+
* @param {boolean | GridProps} [grid] - 是否启用网格布局
|
|
23
|
+
* @param {boolean} [disabled] - 是否禁用所有子字段
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
import {
|
|
28
|
+
InjectionFormKey,
|
|
29
|
+
getObject,
|
|
30
|
+
isObjectItem,
|
|
31
|
+
} from '../../../../shared/core';
|
|
32
|
+
import type { GridProps } from '../../../../shared/ui';
|
|
33
|
+
import { Grid as AGrid, useProviderDisabled } from '../../../../shared/ui';
|
|
34
|
+
import { computed, inject, toRaw, toValue } from 'vue';
|
|
35
|
+
import type { Fields, Grid } from '../../types';
|
|
36
|
+
import BaseFormItem from '../BaseFormItem/index.vue';
|
|
37
|
+
import { getFieldDataKey } from '../../utils/fieldKey';
|
|
38
|
+
import { INJECT_CONFIG } from '../../../component-provider/constants';
|
|
39
|
+
|
|
40
|
+
defineOptions({ name: 'BaseFormItems', inheritAttrs: false });
|
|
41
|
+
|
|
42
|
+
type Props = {
|
|
43
|
+
/** 是否启用网格布局 */
|
|
44
|
+
grid?: Grid;
|
|
45
|
+
/** 字段配置数组 */
|
|
46
|
+
fields?: Fields;
|
|
47
|
+
/** 是否禁用所有子字段 */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
};
|
|
50
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
51
|
+
grid: undefined,
|
|
52
|
+
fields: () => [],
|
|
53
|
+
disabled: undefined,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const validFields = computed(() =>
|
|
57
|
+
// hidden 支持 Ref / ComputedRef 响应式形态(统一使用 toValue 取值)
|
|
58
|
+
(props.fields ?? []).filter(
|
|
59
|
+
field => isObjectItem(field) && !toValue((field as any).hidden)
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// 数组行按数据对象身份派生 key,字段配置重建后行实例保持复用
|
|
64
|
+
const injectedForm = inject(InjectionFormKey, undefined) as any;
|
|
65
|
+
const renderedFields = computed(() => {
|
|
66
|
+
const rawFormData = injectedForm?.formData
|
|
67
|
+
? toRaw(injectedForm.formData)
|
|
68
|
+
: undefined;
|
|
69
|
+
const seen = new Map<string | number, number>();
|
|
70
|
+
return validFields.value.map(field => {
|
|
71
|
+
const baseKey = getFieldDataKey(field, rawFormData);
|
|
72
|
+
const count = seen.get(baseKey) ?? 0;
|
|
73
|
+
seen.set(baseKey, count + 1);
|
|
74
|
+
return {
|
|
75
|
+
field,
|
|
76
|
+
key: count === 0 ? baseKey : `${String(baseKey)}#${count}`,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 向下注入表单级禁用上下文,BaseField 经 useInjectDisabled 消费
|
|
82
|
+
useProviderDisabled(computed(() => props.disabled));
|
|
83
|
+
|
|
84
|
+
const config = INJECT_CONFIG['pro-form'];
|
|
85
|
+
const injectFormConfig = inject(config.injectionKey, config.default);
|
|
86
|
+
const injectGrid = computed(() => toValue(injectFormConfig)?.grid);
|
|
87
|
+
|
|
88
|
+
const enableGrid = computed(() => !!props.grid);
|
|
89
|
+
|
|
90
|
+
const computedGridProps = computed<GridProps>(() => {
|
|
91
|
+
return enableGrid.value
|
|
92
|
+
? { ...getObject(injectGrid.value), ...getObject(props.grid) }
|
|
93
|
+
: {};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 以字段原始对象为 key 登记 FormItem / 控件实例,供 getFormItemRef / getComponentRef 读取;
|
|
98
|
+
* 字段对象被回收后对应条目不再可读。
|
|
99
|
+
*/
|
|
100
|
+
const formItemInstanceMap = new WeakMap<object, any>();
|
|
101
|
+
const componentInstanceMap = new WeakMap<object, any>();
|
|
102
|
+
|
|
103
|
+
/** 已告警过的字段对象(不可写场景去重) */
|
|
104
|
+
const warnedFrozenFields = new WeakSet<object>();
|
|
105
|
+
const defineFieldAccessors = (
|
|
106
|
+
raw: object,
|
|
107
|
+
accessors: Record<string, () => any>
|
|
108
|
+
) => {
|
|
109
|
+
try {
|
|
110
|
+
for (const [key, getter] of Object.entries(accessors)) {
|
|
111
|
+
// 访问器仅用于字段对象读取,不参与属性遍历与字段配置透传
|
|
112
|
+
Object.defineProperty(raw, key, {
|
|
113
|
+
value: getter,
|
|
114
|
+
enumerable: false,
|
|
115
|
+
configurable: true,
|
|
116
|
+
writable: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
if (!warnedFrozenFields.has(raw)) {
|
|
121
|
+
warnedFrozenFields.add(raw);
|
|
122
|
+
console.warn(
|
|
123
|
+
'[pro-components] 字段配置对象不可写(如被 Object.freeze),getFormItemRef / getComponentRef 等实例访问器挂载失败。'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const setFormItemRef = (field: any, el: any, getFormItemProps: () => any) => {
|
|
130
|
+
if (!field || typeof field !== 'object') return;
|
|
131
|
+
const raw = toRaw(field);
|
|
132
|
+
if (el) {
|
|
133
|
+
formItemInstanceMap.set(raw, el);
|
|
134
|
+
defineFieldAccessors(raw, {
|
|
135
|
+
getFormItemRef: () => formItemInstanceMap.get(raw),
|
|
136
|
+
getFormItemComputedProps: getFormItemProps,
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
formItemInstanceMap.delete(raw);
|
|
140
|
+
delete raw.getFormItemRef;
|
|
141
|
+
delete raw.getFormItemComputedProps;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const setComponentRef = (field: any, el: any) => {
|
|
146
|
+
if (!field || typeof field !== 'object') return;
|
|
147
|
+
const raw = toRaw(field);
|
|
148
|
+
if (el) {
|
|
149
|
+
componentInstanceMap.set(raw, el);
|
|
150
|
+
defineFieldAccessors(raw, {
|
|
151
|
+
getComponentRef: () => componentInstanceMap.get(raw)?.getComponentRef(),
|
|
152
|
+
getComponentComputedProps: () =>
|
|
153
|
+
componentInstanceMap.get(raw)?.getComponentComputedProps(),
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
componentInstanceMap.delete(raw);
|
|
157
|
+
delete raw.getComponentRef;
|
|
158
|
+
delete raw.getComponentComputedProps;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
</script>
|
|
162
|
+
|
|
163
|
+
<template>
|
|
164
|
+
<AGrid v-if="enableGrid" v-bind="computedGridProps">
|
|
165
|
+
<BaseFormItem
|
|
166
|
+
v-for="item of renderedFields"
|
|
167
|
+
:key="item.key"
|
|
168
|
+
:field="item.field"
|
|
169
|
+
:grid="props.grid"
|
|
170
|
+
:disabled="props.disabled"
|
|
171
|
+
:set-form-item-ref="setFormItemRef"
|
|
172
|
+
:set-component-ref="setComponentRef"
|
|
173
|
+
/>
|
|
174
|
+
</AGrid>
|
|
175
|
+
<template v-else>
|
|
176
|
+
<BaseFormItem
|
|
177
|
+
v-for="item of renderedFields"
|
|
178
|
+
:key="item.key"
|
|
179
|
+
:field="item.field"
|
|
180
|
+
:grid="props.grid"
|
|
181
|
+
:disabled="props.disabled"
|
|
182
|
+
:set-form-item-ref="setFormItemRef"
|
|
183
|
+
:set-component-ref="setComponentRef"
|
|
184
|
+
/>
|
|
185
|
+
</template>
|
|
186
|
+
</template>
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 架构设计说明 (Architecture Overview)
|
|
8
8
|
*
|
|
9
|
-
* [ 数据层 (Core) ] => [ 绑定层 (ProForm) ] => [ 渲染层 (BaseFormItem / BaseField) ]
|
|
9
|
+
* [ 数据层 (Core) ] => [ 绑定层 (ProForm) ] => [ 渲染层 (BaseFormItems / BaseFormItem / BaseField) ]
|
|
10
10
|
*
|
|
11
11
|
* 1. 数据隔离:ProForm 本身不负责存储和管理表单状态,所有核心状态(formData, fields, 校验规则)
|
|
12
12
|
* 均交由底层的 `useForm`(@qin-ui/pro-components-core)驱动。
|
|
13
13
|
* 2. 上下文透传:通过 `provide` 将核心状态下发给子组件。
|
|
14
|
-
* 3. 递归渲染:网格布局和具体字段的解析交由 `<
|
|
14
|
+
* 3. 递归渲染:网格布局和具体字段的解析交由 `<BaseFormItems>` 递归完成。
|
|
15
15
|
*
|
|
16
16
|
* @public
|
|
17
17
|
*
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
type Slot,
|
|
58
58
|
} from 'vue';
|
|
59
59
|
import { INJECT_CONFIG } from '../../../component-provider/constants';
|
|
60
|
-
import
|
|
60
|
+
import BaseFormItems from '../BaseFormItems/index.vue';
|
|
61
61
|
import { InjectionFormKey, InjectionFormSlotsKey } from '../../constants';
|
|
62
62
|
import type { Grid, VModelProps, PathProps } from '../../types';
|
|
63
63
|
import type { Path } from '../../../../shared/core';
|
|
@@ -82,7 +82,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
82
82
|
/**
|
|
83
83
|
* 依赖注入中心:
|
|
84
84
|
* 未传 form 实例(含运行期置空)时回退内部表单实例;
|
|
85
|
-
* 将 Form 实例注入到 Vue Context 中,供深层子组件(如
|
|
85
|
+
* 将 Form 实例注入到 Vue Context 中,供深层子组件(如 BaseFormItems, BaseField, 自定义插槽)跨层级直接访问数据和方法。
|
|
86
86
|
*/
|
|
87
87
|
// 兜底表单实例:setup 同步期创建,未传 :form(含运行期置空)时经 currentForm 生效
|
|
88
88
|
const fallbackForm = useForm() as unknown as F;
|
|
@@ -165,7 +165,7 @@ provide(InjectionFormSlotsKey, formSlotsRef);
|
|
|
165
165
|
v-bind="formAttrs"
|
|
166
166
|
class="pro-form"
|
|
167
167
|
>
|
|
168
|
-
<
|
|
168
|
+
<BaseFormItems :fields="fields" :grid="props.grid" />
|
|
169
169
|
<slot />
|
|
170
170
|
</AForm>
|
|
171
171
|
</template>
|