@qin-ui/element-plus-pro 1.4.1 → 1.4.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/BaseField/utils/index.d.ts.map +1 -1
- package/es/components/form/components/BaseFormItem/index.vue.d.ts +60 -9
- package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -1
- package/es/components/form/components/BaseFormItems/index.vue.d.ts +13 -0
- package/es/components/form/components/BaseFormItems/index.vue.d.ts.map +1 -0
- package/es/components/form/components/ProForm/index.vue.d.ts.map +1 -1
- 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 +10 -2
- package/es/components/form/hooks/useGroupFieldAttrs.d.ts.map +1 -1
- package/es/components/form/utils/disabled.d.ts +28 -0
- package/es/components/form/utils/disabled.d.ts.map +1 -0
- package/es/components/form/utils/fieldKey.d.ts +12 -0
- package/es/components/form/utils/fieldKey.d.ts.map +1 -1
- package/es/components/table/components/ProTable.vue.d.ts +1 -1
- package/es/components/table/components/ProTable.vue.d.ts.map +1 -1
- package/es/element-plus-pro.css +8 -7
- package/es/index.js +301 -153
- package/es/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/form/components/BaseField/index.vue +6 -8
- package/src/components/form/components/BaseField/utils/index.ts +3 -0
- package/src/components/form/components/BaseFormItem/index.vue +145 -175
- package/src/components/form/components/BaseFormItems/index.vue +183 -0
- package/src/components/form/components/ProForm/index.vue +18 -5
- package/src/components/form/components/index.ts +6 -6
- package/src/components/form/hooks/useGroupFieldAttrs.ts +104 -79
- package/src/components/form/utils/disabled.ts +37 -0
- package/src/components/form/utils/fieldKey.ts +64 -0
- package/src/components/table/components/ProTable.vue +15 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qin-ui/element-plus-pro",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "基于 element-plus 的二次封装组件",
|
|
5
5
|
"bin": {
|
|
6
6
|
"element-plus-pro": "./bin/init-ai.mjs"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"vue-component-type-helpers": "^3.2.5",
|
|
43
|
-
"@qin-ui/pro-components-core": "^2.1.
|
|
43
|
+
"@qin-ui/pro-components-core": "^2.1.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24",
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
} from '../../../component-provider/constants';
|
|
55
55
|
import { useForm } from '../../hooks/useForm';
|
|
56
56
|
import { getInitProps, warnUnknownComponent } from './utils';
|
|
57
|
+
import { resolveEffectiveDisabled } from '../../utils/disabled';
|
|
57
58
|
import { useLocale } from '../../../../locale';
|
|
58
59
|
|
|
59
60
|
const cloneDeepIfObject = (val: unknown) =>
|
|
@@ -161,14 +162,11 @@ const groupedAttrs = computed(() => {
|
|
|
161
162
|
t
|
|
162
163
|
);
|
|
163
164
|
const modelPropName = attrs.modelProp ?? initProps.modelProp ?? 'modelValue';
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
value
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
? normalizeDisabled(attrs.disabled)
|
|
170
|
-
: normalizeDisabled(parentDisabled.value) ||
|
|
171
|
-
normalizeDisabled(initProps.disabled);
|
|
165
|
+
const effectiveDisabled = resolveEffectiveDisabled(
|
|
166
|
+
attrs.disabled,
|
|
167
|
+
parentDisabled.value,
|
|
168
|
+
initProps.disabled
|
|
169
|
+
);
|
|
172
170
|
const mergedProps = mergeProps(
|
|
173
171
|
initProps,
|
|
174
172
|
attrs,
|
|
@@ -21,6 +21,8 @@ const PLACEHOLDER_KEYS: Record<string, string> = {
|
|
|
21
21
|
'date-picker.week': 'date-picker.placeholder',
|
|
22
22
|
'date-picker.month': 'date-picker.placeholder',
|
|
23
23
|
'date-picker.months': 'date-picker.placeholder',
|
|
24
|
+
'date-picker.quarter': 'date-picker.placeholder',
|
|
25
|
+
'date-picker.quarters': 'date-picker.placeholder',
|
|
24
26
|
'date-picker.year': 'date-picker.placeholder',
|
|
25
27
|
'date-picker.years': 'date-picker.placeholder',
|
|
26
28
|
'date-picker.datetime': 'date-picker.placeholder',
|
|
@@ -33,6 +35,7 @@ const RANGE_PLACEHOLDER_KEYS: Record<string, [string, string]> = {
|
|
|
33
35
|
'date-picker.daterange': ['date-picker.start', 'date-picker.end'],
|
|
34
36
|
'date-picker.monthrange': ['date-picker.startMonth', 'date-picker.endMonth'],
|
|
35
37
|
'date-picker.yearrange': ['date-picker.startYear', 'date-picker.endYear'],
|
|
38
|
+
'date-picker.quarterrange': ['date-picker.start', 'date-picker.end'],
|
|
36
39
|
'date-picker.datetimerange': ['date-picker.startTime', 'date-picker.endTime'],
|
|
37
40
|
};
|
|
38
41
|
|
|
@@ -1,204 +1,174 @@
|
|
|
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、error 等 FormItem 插槽)
|
|
7
|
+
* 将单个字段配置渲染为栅格项、FormItem 与控件,支持:
|
|
8
|
+
* - 字段属性分流(grid / formItem / component / slots)
|
|
9
|
+
* - 嵌套字段递归渲染(交由 BaseFormItems)
|
|
10
|
+
* - FormItem / 控件实例登记
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 1. 遍历 fields 数组,过滤 hidden 字段
|
|
17
|
-
* 2. 通过 useGroupFieldAttrs Hook 纯计算将 field 配置拆分为 gridItemProps / formItemProps / componentProps / slots
|
|
18
|
-
* 3. 递归渲染 BaseFormItem(嵌套表单)或 BaseField(输入组件)
|
|
19
|
-
* 4. 通过 ref 回调收集 FormItem 和 Component 实例,注入到 field 对象上
|
|
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
20
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} from '
|
|
39
|
-
import { computed, inject, toRaw, toValue } from 'vue';
|
|
40
|
-
import type { Fields, Grid } from '../../types';
|
|
21
|
+
computed,
|
|
22
|
+
defineComponent,
|
|
23
|
+
h,
|
|
24
|
+
toValue,
|
|
25
|
+
watch,
|
|
26
|
+
type PropType,
|
|
27
|
+
type VNode,
|
|
28
|
+
} from 'vue';
|
|
29
|
+
import { normalizePathKey } from '../../../../shared/core';
|
|
30
|
+
import { FormItem, GridItem as ElGridItem } from '../../../../shared/ui';
|
|
31
|
+
import type { Field, Grid } from '../../types';
|
|
41
32
|
import BaseField from '../BaseField/index.vue';
|
|
33
|
+
import BaseFormItems from '../BaseFormItems/index.vue';
|
|
42
34
|
import SlotComponent from '../SlotComponent/index.vue';
|
|
43
|
-
import
|
|
44
|
-
|
|
45
|
-
|
|
35
|
+
import {
|
|
36
|
+
useGroupedField,
|
|
37
|
+
type GroupedField,
|
|
38
|
+
} from '../../hooks/useGroupFieldAttrs';
|
|
46
39
|
|
|
47
|
-
|
|
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);
|
|
48
69
|
|
|
49
|
-
type
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
// 控件级配置身份:字符串组件名 + type 段;组件对象无 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.type) ?? ''}`;
|
|
76
|
+
});
|
|
77
|
+
const effectiveDisabled = computed(
|
|
78
|
+
() => toValue(props.field.disabled) ?? props.disabled
|
|
79
|
+
);
|
|
59
80
|
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
const setItemRef = (el: any) => {
|
|
82
|
+
props.setFormItemRef?.(
|
|
83
|
+
props.field,
|
|
84
|
+
el,
|
|
85
|
+
() => grouped.value.formItemProps
|
|
86
|
+
);
|
|
87
|
+
};
|
|
62
88
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
field => isObjectItem(field) && !toValue((field as any).hidden)
|
|
67
|
-
)
|
|
68
|
-
);
|
|
89
|
+
const setFieldRef = (el: any) => {
|
|
90
|
+
props.setComponentRef?.(props.field, el);
|
|
91
|
+
};
|
|
69
92
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
// 字段对象变化后,实例访问器归属当前字段
|
|
94
|
+
watch(
|
|
95
|
+
() => props.field,
|
|
96
|
+
(next, prev) => {
|
|
97
|
+
if (!prev || prev === next) return;
|
|
98
|
+
props.setFormItemRef?.(prev, null, () => grouped.value.formItemProps);
|
|
99
|
+
props.setComponentRef?.(prev, null);
|
|
100
|
+
},
|
|
101
|
+
{ flush: 'pre' }
|
|
102
|
+
);
|
|
73
103
|
|
|
74
|
-
|
|
104
|
+
/** 按容器组件逐层包裹当前节点 */
|
|
105
|
+
const withContainer = (
|
|
106
|
+
node: VNode,
|
|
107
|
+
component: any,
|
|
108
|
+
containerProps: Record<string, any>
|
|
109
|
+
): VNode =>
|
|
110
|
+
component ? h(component, containerProps, { default: () => node }) : node;
|
|
75
111
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
/** 组装 FormItem 的动态插槽与默认插槽 */
|
|
113
|
+
const renderFormItemSlots = (groupedField: GroupedField) => {
|
|
114
|
+
const slots: Record<string, any> = {};
|
|
115
|
+
Object.entries(groupedField.formItemSlots).forEach(([name, slot]) => {
|
|
116
|
+
slots[name] = (scope?: Record<string, any>) =>
|
|
117
|
+
h(SlotComponent as any, {
|
|
118
|
+
path: groupedField.path,
|
|
119
|
+
component: slot,
|
|
120
|
+
...(scope || {}),
|
|
121
|
+
});
|
|
122
|
+
});
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
124
|
+
slots.default = () => {
|
|
125
|
+
const field = props.field as any;
|
|
126
|
+
if (field.fields) {
|
|
127
|
+
return h(BaseFormItems as any, {
|
|
128
|
+
grid: field.grid ?? props.grid,
|
|
129
|
+
fields: field.fields,
|
|
130
|
+
disabled: effectiveDisabled.value,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return h(BaseField as any, {
|
|
134
|
+
key: componentKey.value,
|
|
135
|
+
ref: setFieldRef,
|
|
136
|
+
...groupedField.componentProps,
|
|
137
|
+
path: groupedField.path,
|
|
138
|
+
disabled: effectiveDisabled.value,
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
return slots;
|
|
142
|
+
};
|
|
88
143
|
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
144
|
+
return () => {
|
|
145
|
+
const groupedField = grouped.value;
|
|
146
|
+
let node: VNode = h(
|
|
147
|
+
FormItem as any,
|
|
148
|
+
{
|
|
149
|
+
...groupedField.formItemProps,
|
|
150
|
+
ref: setItemRef,
|
|
151
|
+
prop: normalizePathKey(groupedField.path),
|
|
152
|
+
path: groupedField.path,
|
|
153
|
+
},
|
|
154
|
+
renderFormItemSlots(groupedField)
|
|
155
|
+
);
|
|
156
|
+
node = withContainer(node, groupedField.container, {
|
|
157
|
+
path: groupedField.path,
|
|
103
158
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
console.warn(
|
|
109
|
-
'[pro-components] 字段配置对象不可写(如被 Object.freeze),getFormItemRef / getComponentRef 等实例访问器挂载失败。'
|
|
159
|
+
node = withContainer(
|
|
160
|
+
node,
|
|
161
|
+
enableGrid.value ? ElGridItem : undefined,
|
|
162
|
+
groupedField.gridItemProps
|
|
110
163
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const setFormItemRef = (field: any, el: any, formItemProps?: any) => {
|
|
116
|
-
if (!field || typeof field !== 'object') return;
|
|
117
|
-
const raw = toRaw(field);
|
|
118
|
-
if (el) {
|
|
119
|
-
formItemInstanceMap.set(raw, el);
|
|
120
|
-
defineFieldAccessors(raw, {
|
|
121
|
-
getFormItemRef: () => formItemInstanceMap.get(raw),
|
|
122
|
-
getFormItemComputedProps: () => formItemProps,
|
|
123
|
-
});
|
|
124
|
-
} else {
|
|
125
|
-
formItemInstanceMap.delete(raw);
|
|
126
|
-
delete raw.getFormItemRef;
|
|
127
|
-
delete raw.getFormItemComputedProps;
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const setComponentRef = (field: any, el: any) => {
|
|
132
|
-
if (!field || typeof field !== 'object') return;
|
|
133
|
-
const raw = toRaw(field);
|
|
134
|
-
if (el) {
|
|
135
|
-
componentInstanceMap.set(raw, el);
|
|
136
|
-
defineFieldAccessors(raw, {
|
|
137
|
-
getComponentRef: () => componentInstanceMap.get(raw)?.getComponentRef(),
|
|
138
|
-
getComponentComputedProps: () =>
|
|
139
|
-
componentInstanceMap.get(raw)?.getComponentComputedProps(),
|
|
140
|
-
});
|
|
141
|
-
} else {
|
|
142
|
-
componentInstanceMap.delete(raw);
|
|
143
|
-
delete raw.getComponentRef;
|
|
144
|
-
delete raw.getComponentComputedProps;
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const groupedFields = useGroupFieldAttrs(validFields);
|
|
164
|
+
return node;
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
});
|
|
149
168
|
</script>
|
|
150
169
|
|
|
151
|
-
<template>
|
|
152
|
-
<ContainerFragment
|
|
153
|
-
:component="enableGrid ? ElGrid : undefined"
|
|
154
|
-
v-bind="computedGridProps"
|
|
155
|
-
style="width: 100%"
|
|
156
|
-
>
|
|
157
|
-
<template v-for="item of groupedFields" :key="item.renderKey">
|
|
158
|
-
<ContainerFragment
|
|
159
|
-
:component="enableGrid ? ElGridItem : undefined"
|
|
160
|
-
v-bind="item.gridItemProps"
|
|
161
|
-
>
|
|
162
|
-
<ContainerFragment :component="item.container" :path="item.path">
|
|
163
|
-
<FormItem
|
|
164
|
-
v-bind="item.formItemProps"
|
|
165
|
-
:ref="el => setFormItemRef(item.rawField, el, item.formItemProps)"
|
|
166
|
-
:prop="normalizePathKey(item.path)"
|
|
167
|
-
:path="item.path"
|
|
168
|
-
>
|
|
169
|
-
<template
|
|
170
|
-
v-for="(slot, name) in item.formItemSlots"
|
|
171
|
-
:key="name"
|
|
172
|
-
#[name]="scoped"
|
|
173
|
-
>
|
|
174
|
-
<SlotComponent
|
|
175
|
-
:path="item.path"
|
|
176
|
-
:component="slot"
|
|
177
|
-
v-bind="scoped"
|
|
178
|
-
/>
|
|
179
|
-
</template>
|
|
180
|
-
<template v-if="item.rawField.fields">
|
|
181
|
-
<BaseFormItem
|
|
182
|
-
:grid="(item.rawField as any).grid ?? props.grid"
|
|
183
|
-
:fields="item.rawField.fields"
|
|
184
|
-
:disabled="toValue(item.rawField.disabled)"
|
|
185
|
-
/>
|
|
186
|
-
</template>
|
|
187
|
-
<template v-else>
|
|
188
|
-
<BaseField
|
|
189
|
-
:ref="el => setComponentRef(item.rawField, el)"
|
|
190
|
-
v-bind="item.componentProps"
|
|
191
|
-
:path="item.path"
|
|
192
|
-
/>
|
|
193
|
-
</template>
|
|
194
|
-
</FormItem>
|
|
195
|
-
</ContainerFragment>
|
|
196
|
-
</ContainerFragment>
|
|
197
|
-
</template>
|
|
198
|
-
</ContainerFragment>
|
|
199
|
-
</template>
|
|
200
|
-
|
|
201
170
|
<style scoped lang="less">
|
|
171
|
+
/* 分组字段的嵌套 FormItem 间距 */
|
|
202
172
|
[class*='-form-item'] [class*='-form-item'] {
|
|
203
173
|
margin-bottom: 18px;
|
|
204
174
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* 表单字段渲染引擎
|
|
4
|
+
*
|
|
5
|
+
* @component BaseFormItems
|
|
6
|
+
*
|
|
7
|
+
* 负责将字段配置数组渲染为实际的表单项,支持:
|
|
8
|
+
* - 字段分组(嵌套 fields 递归渲染,嵌套字段 path 须为完整点分路径)
|
|
9
|
+
* - 网格布局(grid 属性控制)
|
|
10
|
+
* - FormItem 包装(label、rules、校验等)
|
|
11
|
+
* - 组件绑定(通过 BaseField 渲染具体组件)
|
|
12
|
+
* - 插槽透传(label、error 等 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 ElGrid, 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
|
+
grid?: Grid;
|
|
44
|
+
fields?: Fields;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
};
|
|
47
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
48
|
+
grid: undefined,
|
|
49
|
+
fields: () => [],
|
|
50
|
+
disabled: undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 分组字段的禁用状态向下注入,供 BaseField 经禁用上下文消费
|
|
54
|
+
useProviderDisabled(computed(() => props.disabled));
|
|
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
|
+
const config = INJECT_CONFIG['pro-form'];
|
|
82
|
+
const injectFormConfig = inject(config.injectionKey, config.default);
|
|
83
|
+
const injectGrid = computed(() => toValue(injectFormConfig)?.grid);
|
|
84
|
+
|
|
85
|
+
const enableGrid = computed(() => !!props.grid);
|
|
86
|
+
|
|
87
|
+
const computedGridProps = computed<GridProps>(() => {
|
|
88
|
+
return enableGrid.value
|
|
89
|
+
? { ...getObject(injectGrid.value), ...getObject(props.grid) }
|
|
90
|
+
: {};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 以字段原始对象为 key 登记 FormItem / 控件实例,供 getFormItemRef / getComponentRef 读取;
|
|
95
|
+
* 字段对象被回收后对应条目不再可读。
|
|
96
|
+
*/
|
|
97
|
+
const formItemInstanceMap = new WeakMap<object, any>();
|
|
98
|
+
const componentInstanceMap = new WeakMap<object, any>();
|
|
99
|
+
|
|
100
|
+
/** 已告警过的字段对象(不可写场景去重) */
|
|
101
|
+
const warnedFrozenFields = new WeakSet<object>();
|
|
102
|
+
const defineFieldAccessors = (
|
|
103
|
+
raw: object,
|
|
104
|
+
accessors: Record<string, () => any>
|
|
105
|
+
) => {
|
|
106
|
+
try {
|
|
107
|
+
for (const [key, getter] of Object.entries(accessors)) {
|
|
108
|
+
// 访问器仅用于字段对象读取,不参与属性遍历与字段配置透传
|
|
109
|
+
Object.defineProperty(raw, key, {
|
|
110
|
+
value: getter,
|
|
111
|
+
enumerable: false,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
if (!warnedFrozenFields.has(raw)) {
|
|
118
|
+
warnedFrozenFields.add(raw);
|
|
119
|
+
console.warn(
|
|
120
|
+
'[pro-components] 字段配置对象不可写(如被 Object.freeze),getFormItemRef / getComponentRef 等实例访问器挂载失败。'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const setFormItemRef = (field: any, el: any, getFormItemProps: () => any) => {
|
|
127
|
+
if (!field || typeof field !== 'object') return;
|
|
128
|
+
const raw = toRaw(field);
|
|
129
|
+
if (el) {
|
|
130
|
+
formItemInstanceMap.set(raw, el);
|
|
131
|
+
defineFieldAccessors(raw, {
|
|
132
|
+
getFormItemRef: () => formItemInstanceMap.get(raw),
|
|
133
|
+
getFormItemComputedProps: getFormItemProps,
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
formItemInstanceMap.delete(raw);
|
|
137
|
+
delete raw.getFormItemRef;
|
|
138
|
+
delete raw.getFormItemComputedProps;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const setComponentRef = (field: any, el: any) => {
|
|
143
|
+
if (!field || typeof field !== 'object') return;
|
|
144
|
+
const raw = toRaw(field);
|
|
145
|
+
if (el) {
|
|
146
|
+
componentInstanceMap.set(raw, el);
|
|
147
|
+
defineFieldAccessors(raw, {
|
|
148
|
+
getComponentRef: () => componentInstanceMap.get(raw)?.getComponentRef(),
|
|
149
|
+
getComponentComputedProps: () =>
|
|
150
|
+
componentInstanceMap.get(raw)?.getComponentComputedProps(),
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
componentInstanceMap.delete(raw);
|
|
154
|
+
delete raw.getComponentRef;
|
|
155
|
+
delete raw.getComponentComputedProps;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
<template>
|
|
161
|
+
<ElGrid v-if="enableGrid" v-bind="computedGridProps" style="width: 100%">
|
|
162
|
+
<BaseFormItem
|
|
163
|
+
v-for="item of renderedFields"
|
|
164
|
+
:key="item.key"
|
|
165
|
+
:field="item.field"
|
|
166
|
+
:grid="props.grid"
|
|
167
|
+
:disabled="props.disabled"
|
|
168
|
+
:set-form-item-ref="setFormItemRef"
|
|
169
|
+
:set-component-ref="setComponentRef"
|
|
170
|
+
/>
|
|
171
|
+
</ElGrid>
|
|
172
|
+
<template v-else>
|
|
173
|
+
<BaseFormItem
|
|
174
|
+
v-for="item of renderedFields"
|
|
175
|
+
:key="item.key"
|
|
176
|
+
:field="item.field"
|
|
177
|
+
:grid="props.grid"
|
|
178
|
+
:disabled="props.disabled"
|
|
179
|
+
:set-form-item-ref="setFormItemRef"
|
|
180
|
+
:set-component-ref="setComponentRef"
|
|
181
|
+
/>
|
|
182
|
+
</template>
|
|
183
|
+
</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
|
*
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
import type { FormProps as ElFormProps } from '../../../../shared/ui';
|
|
47
|
-
import { Form as ElForm } from '../../../../shared/ui';
|
|
47
|
+
import { Form as ElForm, useDisabledContext } from '../../../../shared/ui';
|
|
48
48
|
import {
|
|
49
49
|
computed,
|
|
50
50
|
inject,
|
|
@@ -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';
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
useReactiveAttrs,
|
|
69
69
|
} from '../../../../shared/core';
|
|
70
70
|
import { useForm, type Form } from '../../hooks/useForm';
|
|
71
|
+
import { resolveDisabled } from '../../utils/disabled';
|
|
71
72
|
|
|
72
73
|
defineOptions({ name: 'ProForm', inheritAttrs: false });
|
|
73
74
|
|
|
@@ -141,6 +142,14 @@ const formAttrs = computed(() => {
|
|
|
141
142
|
);
|
|
142
143
|
});
|
|
143
144
|
|
|
145
|
+
// 表单级禁用声明:显式值(含 false)优先,未声明时仅继承外层 true
|
|
146
|
+
const outerDisabled = useDisabledContext();
|
|
147
|
+
const formDisabled = computed<boolean | undefined>(() => {
|
|
148
|
+
const explicit = resolveDisabled(toValue(formAttrs.value.disabled));
|
|
149
|
+
if (explicit !== undefined) return explicit;
|
|
150
|
+
return outerDisabled.value ? true : undefined;
|
|
151
|
+
});
|
|
152
|
+
|
|
144
153
|
type ExtractPath<T> = T extends Form<infer D> ? Path<D> : string;
|
|
145
154
|
type FieldSlotProps = VModelProps &
|
|
146
155
|
PathProps & { disabled?: boolean; [x: string]: any };
|
|
@@ -169,7 +178,11 @@ provide(InjectionFormSlotsKey, formSlotsRef);
|
|
|
169
178
|
class="pro-form"
|
|
170
179
|
@submit.prevent
|
|
171
180
|
>
|
|
172
|
-
<
|
|
181
|
+
<BaseFormItems
|
|
182
|
+
:fields="fields"
|
|
183
|
+
:grid="props.grid"
|
|
184
|
+
:disabled="formDisabled"
|
|
185
|
+
/>
|
|
173
186
|
<slot />
|
|
174
187
|
</ElForm>
|
|
175
188
|
</template>
|