@qin-ui/vant-pro 1.0.4 → 1.1.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/AI-CONTEXT.md +165 -23
- package/LICENSE +9 -0
- package/README.md +1 -1
- package/api.json +27 -22
- package/bin/init-ai.mjs +242 -52
- package/es/component-provider/{index-DC_k4_R3.js → index-cldbPMDQ.js} +5 -4
- package/es/component-provider/index-cldbPMDQ.js.map +1 -0
- package/es/component-provider/index.js +2 -1
- package/es/component-provider/index.js.map +1 -0
- package/es/components/component-provider/components/index.vue.d.ts +27 -0
- package/es/components/component-provider/components/index.vue.d.ts.map +1 -0
- package/es/components/component-provider/constants/index.d.ts +16 -0
- package/es/components/component-provider/constants/index.d.ts.map +1 -0
- package/es/components/component-provider/index.d.ts +4 -0
- package/es/components/component-provider/index.d.ts.map +1 -0
- package/es/components/component-provider/types/index.d.ts +16 -0
- package/es/components/component-provider/types/index.d.ts.map +1 -0
- package/es/components/form/components/BaseField/index.vue.d.ts +1323 -0
- package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -0
- package/es/components/form/components/BaseField/utils/index.d.ts +3 -0
- package/es/components/form/components/BaseField/utils/index.d.ts.map +1 -0
- package/es/components/form/components/BaseFormItem/index.vue.d.ts +7 -0
- package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -0
- package/es/components/form/components/ContainerFragment/index.vue.d.ts +23 -0
- package/es/components/form/components/ContainerFragment/index.vue.d.ts.map +1 -0
- package/es/components/form/components/GroupedFieldAttrs/index.vue.d.ts +24 -0
- package/es/components/form/components/GroupedFieldAttrs/index.vue.d.ts.map +1 -0
- package/es/components/form/components/PathProvider/index.vue.d.ts +23 -0
- package/es/components/form/components/PathProvider/index.vue.d.ts.map +1 -0
- package/es/components/form/components/ProForm/index.vue.d.ts +34 -0
- package/es/components/form/components/ProForm/index.vue.d.ts.map +1 -0
- package/es/components/form/components/SlotComponent/index.vue.d.ts +7 -0
- package/es/components/form/components/SlotComponent/index.vue.d.ts.map +1 -0
- package/es/components/form/components/index.d.ts +12 -0
- package/es/components/form/components/index.d.ts.map +1 -0
- package/es/components/form/constants/index.d.ts +62 -0
- package/es/components/form/constants/index.d.ts.map +1 -0
- package/es/components/form/hooks/index.d.ts +6 -0
- package/es/components/form/hooks/index.d.ts.map +1 -0
- package/es/components/form/hooks/useFields.d.ts +57 -0
- package/es/components/form/hooks/useFields.d.ts.map +1 -0
- package/es/components/form/hooks/useForm.d.ts +101 -0
- package/es/components/form/hooks/useForm.d.ts.map +1 -0
- package/es/components/form/hooks/useFormPopup.d.ts +57 -0
- package/es/components/form/hooks/useFormPopup.d.ts.map +1 -0
- package/es/components/form/hooks/useFormRef.d.ts +30 -0
- package/es/components/form/hooks/useFormRef.d.ts.map +1 -0
- package/es/components/form/index.d.ts +7 -0
- package/es/components/form/index.d.ts.map +1 -0
- package/es/components/form/types/index.d.ts +152 -0
- package/es/components/form/types/index.d.ts.map +1 -0
- package/es/form/index.js +11 -7
- package/es/form/index.js.map +1 -0
- package/es/index.d.ts +58 -954
- package/es/index.d.ts.map +1 -0
- package/es/index.js +10 -21
- package/es/index.js.map +1 -0
- package/es/shared/core/index.d.ts +6 -0
- package/es/shared/core/index.d.ts.map +1 -0
- package/es/shared/ui/index.d.ts +2 -0
- package/es/shared/ui/index.d.ts.map +1 -0
- package/package.json +11 -12
- package/src/components/component-provider/components/index.vue +67 -0
- package/src/components/component-provider/constants/index.ts +110 -0
- package/src/components/component-provider/index.ts +6 -0
- package/src/components/component-provider/types/index.ts +31 -0
- package/src/components/form/components/BaseField/index.vue +340 -0
- package/src/components/form/components/BaseField/utils/index.ts +14 -0
- package/src/components/form/components/BaseFormItem/index.vue +77 -0
- package/src/components/form/components/ContainerFragment/index.vue +24 -0
- package/src/components/form/components/GroupedFieldAttrs/index.vue +51 -0
- package/src/components/form/components/PathProvider/index.vue +18 -0
- package/src/components/form/components/ProForm/index.vue +114 -0
- package/src/components/form/components/SlotComponent/index.vue +26 -0
- package/src/components/form/components/index.ts +15 -0
- package/src/components/form/constants/index.ts +109 -0
- package/src/components/form/hooks/index.ts +6 -0
- package/src/components/form/hooks/useFields.ts +67 -0
- package/src/components/form/hooks/useForm.ts +126 -0
- package/src/components/form/hooks/useFormPopup.ts +111 -0
- package/src/components/form/hooks/useFormRef.ts +35 -0
- package/src/components/form/index.ts +27 -0
- package/src/components/form/types/index.ts +220 -0
- package/src/index.ts +62 -0
- package/src/shared/core/index.ts +37 -0
- package/src/shared/ui/index.ts +26 -0
- package/es/core/index-B5vvm4Fj.js +0 -236
- package/es/vendor/utils/lodash-es-DN4QDiDm.js +0 -1079
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* @component BaseField
|
|
4
|
+
* @description 字段级渲染组件,负责将 field 配置渲染为实际的输入组件
|
|
5
|
+
*
|
|
6
|
+
* ## 核心职责
|
|
7
|
+
*
|
|
8
|
+
* 1. **组件解析**:按优先级解析要渲染的组件(teleport 插槽 > 自定义组件 > 内置组件映射 > 原始值)
|
|
9
|
+
* 2. **v-model 双向绑定**:通过 computed getter/setter 将组件值与 formData 的深层路径绑定
|
|
10
|
+
* 3. **valueFormatter 处理**:支持 get/set 格式化函数,用于数据转换(如日期格式化)
|
|
11
|
+
* 4. **属性合并**:将注入的默认 props、组件传入的 attrs 合并为最终属性
|
|
12
|
+
*
|
|
13
|
+
* ## 组件解析优先级
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* teleport 组件(ProForm 插槽注入)
|
|
17
|
+
* ↓ 未匹配
|
|
18
|
+
* 自定义组件(ProComponentProvider 的 componentMap)
|
|
19
|
+
* ↓ 未匹配
|
|
20
|
+
* 内置组件映射(componentMap: input → Input, select → Select ...)
|
|
21
|
+
* ↓ 未匹配
|
|
22
|
+
* 原始 component 值(可能是字符串或 Vue 组件对象)
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param {string | Component} [component] - 组件名或组件对象
|
|
26
|
+
* @param {string} [path] - 字段路径,用于绑定表单数据
|
|
27
|
+
*
|
|
28
|
+
* @slot default - 默认插槽(透传给组件)
|
|
29
|
+
*/
|
|
30
|
+
import {
|
|
31
|
+
type Component,
|
|
32
|
+
computed,
|
|
33
|
+
inject,
|
|
34
|
+
mergeProps,
|
|
35
|
+
ref,
|
|
36
|
+
Teleport,
|
|
37
|
+
useAttrs,
|
|
38
|
+
} from 'vue';
|
|
39
|
+
import { cloneDeep, omit } from '../../../../shared/core';
|
|
40
|
+
import type { Field as FieldType } from '../../types';
|
|
41
|
+
import { ContainerFragment, SlotComponent } from '..';
|
|
42
|
+
import { componentMap, TeleportComponentNamePrefix } from '../../constants';
|
|
43
|
+
import { INJECT_COMPONENTS } from '../../../component-provider/constants';
|
|
44
|
+
import { useForm } from '../../hooks/useForm';
|
|
45
|
+
import { getInitProps } from './utils';
|
|
46
|
+
import { Field as VanField } from '../../../../shared/ui';
|
|
47
|
+
|
|
48
|
+
defineOptions({ name: 'BaseField', inheritAttrs: false });
|
|
49
|
+
|
|
50
|
+
const uiFieldPropsKeys = Object.keys(VanField.props || {});
|
|
51
|
+
|
|
52
|
+
type Props = {
|
|
53
|
+
component?: string | Component;
|
|
54
|
+
path?: string;
|
|
55
|
+
};
|
|
56
|
+
const { component = undefined, path = '' } = defineProps<Props>();
|
|
57
|
+
|
|
58
|
+
const form = useForm(false);
|
|
59
|
+
const { getFormData, setFormData, formPopup } = form;
|
|
60
|
+
const { popupFieldPath, popupFieldValue } = formPopup;
|
|
61
|
+
|
|
62
|
+
const componentRef = ref<any>();
|
|
63
|
+
const fieldRef = ref<any>();
|
|
64
|
+
|
|
65
|
+
const attrs: any = useAttrs();
|
|
66
|
+
|
|
67
|
+
function getOldValue() {
|
|
68
|
+
return cloneDeep(getFormData?.(path));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// setter: 向 formData 写入值,支持 valueFormatter / valueFormatter.set 转换
|
|
72
|
+
const value = computed({
|
|
73
|
+
get() {
|
|
74
|
+
let val = getFormData?.(path);
|
|
75
|
+
const { valueFormatter } = groupedAttrs.value;
|
|
76
|
+
if (
|
|
77
|
+
typeof valueFormatter === 'object' &&
|
|
78
|
+
typeof valueFormatter.get === 'function'
|
|
79
|
+
) {
|
|
80
|
+
val = valueFormatter.get(val);
|
|
81
|
+
}
|
|
82
|
+
return val;
|
|
83
|
+
},
|
|
84
|
+
set(val) {
|
|
85
|
+
let newVal = val;
|
|
86
|
+
const { valueFormatter } = groupedAttrs.value;
|
|
87
|
+
if (valueFormatter) {
|
|
88
|
+
if (typeof valueFormatter === 'function') {
|
|
89
|
+
newVal = valueFormatter(val, getOldValue());
|
|
90
|
+
} else if (typeof valueFormatter.set === 'function') {
|
|
91
|
+
newVal = valueFormatter.set(val, getOldValue());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
setFormData?.(path, newVal);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const popupCloseEventName = computed(() => {
|
|
99
|
+
let confirm = 'onConfirm';
|
|
100
|
+
let cancel = 'onCancel';
|
|
101
|
+
if (component === 'cascader') {
|
|
102
|
+
confirm = 'onFinish';
|
|
103
|
+
cancel = 'onClose';
|
|
104
|
+
} else if (component === 'signature') {
|
|
105
|
+
confirm = 'onSubmit';
|
|
106
|
+
}
|
|
107
|
+
return { confirm, cancel };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* 将注入的默认 props、外部传入的 attrs、field 配置合并为最终属性
|
|
112
|
+
* 属性分为多类:组件绑定属性、Field UI 属性等
|
|
113
|
+
*/
|
|
114
|
+
const groupedAttrs = computed(() => {
|
|
115
|
+
const initProps = getInitProps({
|
|
116
|
+
component: component,
|
|
117
|
+
type: attrs.type,
|
|
118
|
+
} as unknown as FieldType);
|
|
119
|
+
const modelPropName = attrs.modelProp ?? initProps.modelProp ?? 'modelValue';
|
|
120
|
+
const mergedProps = mergeProps(
|
|
121
|
+
initProps,
|
|
122
|
+
attrs,
|
|
123
|
+
{ disabled: attrs.disabled ?? initProps.disabled },
|
|
124
|
+
{ modelProp: modelPropName }
|
|
125
|
+
) as Required<FieldType>;
|
|
126
|
+
|
|
127
|
+
// prettier-ignore
|
|
128
|
+
const { valueFormatter, displayFormatter, modelProp, slots, fieldClass, fieldStyle, componentContainer, popup, ...rest } = mergedProps
|
|
129
|
+
const modelBindingProp = modelProp ?? 'modelValue';
|
|
130
|
+
// 从剩余属性中移除 v-model 绑定属性,避免重复绑定
|
|
131
|
+
const bindAttrs = omit(rest as Record<string, any>, [
|
|
132
|
+
modelBindingProp,
|
|
133
|
+
`onUpdate:${modelBindingProp}`,
|
|
134
|
+
]);
|
|
135
|
+
if (popup) {
|
|
136
|
+
const {
|
|
137
|
+
[popupCloseEventName.value.confirm]: confirmEvent,
|
|
138
|
+
[popupCloseEventName.value.cancel]: cancelEvent,
|
|
139
|
+
} = bindAttrs as any;
|
|
140
|
+
Object.assign(bindAttrs, {
|
|
141
|
+
[popupCloseEventName.value.confirm]: (...args: any[]) => {
|
|
142
|
+
value.value = popupFieldValue.value;
|
|
143
|
+
formPopup.close();
|
|
144
|
+
confirmEvent?.(...args);
|
|
145
|
+
},
|
|
146
|
+
[popupCloseEventName.value.cancel]: (...args: any[]) => {
|
|
147
|
+
formPopup.close();
|
|
148
|
+
cancelEvent?.(...args);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fieldAttrs = Object.keys(bindAttrs).reduce(
|
|
154
|
+
(acc, key) => {
|
|
155
|
+
if (
|
|
156
|
+
(uiFieldPropsKeys.includes(key) || key.startsWith('on')) &&
|
|
157
|
+
key !== 'type'
|
|
158
|
+
) {
|
|
159
|
+
acc[key] = (bindAttrs as any)[key];
|
|
160
|
+
}
|
|
161
|
+
return acc;
|
|
162
|
+
},
|
|
163
|
+
{ class: fieldClass, style: fieldStyle } as Record<string, any>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
attrs: bindAttrs,
|
|
168
|
+
fieldAttrs,
|
|
169
|
+
slots,
|
|
170
|
+
componentContainer,
|
|
171
|
+
modelProp: modelBindingProp,
|
|
172
|
+
valueFormatter,
|
|
173
|
+
displayFormatter,
|
|
174
|
+
popup,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 从 ProForm 插槽注入的 teleport 组件(优先级最高)
|
|
179
|
+
const teleportComponent = inject(
|
|
180
|
+
`${TeleportComponentNamePrefix}${path}`,
|
|
181
|
+
undefined
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// 从 ProComponentProvider 注入的自定义组件映射
|
|
185
|
+
const customComponents = inject(INJECT_COMPONENTS, {});
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
* 按优先级解析最终要渲染的组件:
|
|
189
|
+
* teleport 插槽 → 自定义组件映射 → 内置组件映射 → 原始 component 值
|
|
190
|
+
*/
|
|
191
|
+
const is = computed(() => {
|
|
192
|
+
if (teleportComponent) return teleportComponent;
|
|
193
|
+
if (typeof component === 'string') {
|
|
194
|
+
return (
|
|
195
|
+
(customComponents as any)[component] ||
|
|
196
|
+
(componentMap as any)[component] ||
|
|
197
|
+
component
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return component;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 处理 Popup 相关的逻辑
|
|
204
|
+
const popupAble = computed(() => !!groupedAttrs.value.popup);
|
|
205
|
+
const popupContainer = computed(() =>
|
|
206
|
+
typeof groupedAttrs.value.popup === 'boolean'
|
|
207
|
+
? undefined
|
|
208
|
+
: groupedAttrs.value.popup?.container
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// 获取要展示在只读 Field 上的文字
|
|
212
|
+
const displayValue = computed(() => {
|
|
213
|
+
const { displayFormatter } = groupedAttrs.value;
|
|
214
|
+
if (displayFormatter && typeof displayFormatter === 'function') {
|
|
215
|
+
return displayFormatter(value.value);
|
|
216
|
+
}
|
|
217
|
+
return value.value;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const onFieldClick = () => {
|
|
221
|
+
const attrs = groupedAttrs.value.attrs as any;
|
|
222
|
+
if (!popupAble.value || attrs.disabled || attrs.readonly) return;
|
|
223
|
+
|
|
224
|
+
const popupProps =
|
|
225
|
+
typeof groupedAttrs.value.popup === 'object'
|
|
226
|
+
? groupedAttrs.value.popup
|
|
227
|
+
: {};
|
|
228
|
+
formPopup.updateProps(popupProps);
|
|
229
|
+
popupFieldValue.value = value.value;
|
|
230
|
+
formPopup.open(path);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
defineExpose({
|
|
234
|
+
getComponentRef: () => componentRef.value || fieldRef.value,
|
|
235
|
+
getComponentComputedProps: () => groupedAttrs.value.attrs as any,
|
|
236
|
+
});
|
|
237
|
+
</script>
|
|
238
|
+
|
|
239
|
+
<template>
|
|
240
|
+
<ContainerFragment :component="groupedAttrs.componentContainer" :path="path">
|
|
241
|
+
<template v-if="popupAble">
|
|
242
|
+
<VanField
|
|
243
|
+
ref="fieldRef"
|
|
244
|
+
v-bind="{
|
|
245
|
+
readonly: true,
|
|
246
|
+
clickable: true,
|
|
247
|
+
isLink: true,
|
|
248
|
+
...groupedAttrs.fieldAttrs,
|
|
249
|
+
}"
|
|
250
|
+
:model-value="displayValue"
|
|
251
|
+
@click="onFieldClick"
|
|
252
|
+
/>
|
|
253
|
+
<ContainerFragment
|
|
254
|
+
v-if="popupFieldPath === path"
|
|
255
|
+
:component="Teleport"
|
|
256
|
+
to="#pro-form-popup-content"
|
|
257
|
+
defer
|
|
258
|
+
>
|
|
259
|
+
<ContainerFragment :component="popupContainer" :path="path">
|
|
260
|
+
<component
|
|
261
|
+
:is="is"
|
|
262
|
+
v-if="is"
|
|
263
|
+
v-bind="groupedAttrs.attrs"
|
|
264
|
+
v-model:[`${groupedAttrs.modelProp}`]="popupFieldValue"
|
|
265
|
+
>
|
|
266
|
+
<template
|
|
267
|
+
v-for="(slot, name) in groupedAttrs.slots"
|
|
268
|
+
:key="name"
|
|
269
|
+
#[name]="scoped"
|
|
270
|
+
>
|
|
271
|
+
<SlotComponent :path="path" v-bind="scoped" :component="slot" />
|
|
272
|
+
</template>
|
|
273
|
+
</component>
|
|
274
|
+
</ContainerFragment>
|
|
275
|
+
</ContainerFragment>
|
|
276
|
+
</template>
|
|
277
|
+
|
|
278
|
+
<template v-else-if="typeof component === 'string'">
|
|
279
|
+
<component
|
|
280
|
+
:is="is"
|
|
281
|
+
v-if="component === 'field'"
|
|
282
|
+
v-bind="groupedAttrs.attrs"
|
|
283
|
+
ref="componentRef"
|
|
284
|
+
v-model:[`${groupedAttrs.modelProp}`]="value"
|
|
285
|
+
:name="path"
|
|
286
|
+
>
|
|
287
|
+
<template
|
|
288
|
+
v-for="(slot, name) in groupedAttrs.slots"
|
|
289
|
+
:key="name"
|
|
290
|
+
#[name]="scoped"
|
|
291
|
+
>
|
|
292
|
+
<SlotComponent :path="path" v-bind="scoped" :component="slot" />
|
|
293
|
+
</template>
|
|
294
|
+
</component>
|
|
295
|
+
|
|
296
|
+
<VanField
|
|
297
|
+
v-else
|
|
298
|
+
ref="fieldRef"
|
|
299
|
+
v-bind="groupedAttrs.fieldAttrs"
|
|
300
|
+
:name="path"
|
|
301
|
+
>
|
|
302
|
+
<template #input>
|
|
303
|
+
<component
|
|
304
|
+
:is="is"
|
|
305
|
+
v-if="is"
|
|
306
|
+
v-bind="groupedAttrs.attrs"
|
|
307
|
+
ref="componentRef"
|
|
308
|
+
v-model:[`${groupedAttrs.modelProp}`]="value"
|
|
309
|
+
>
|
|
310
|
+
<template
|
|
311
|
+
v-for="(slot, name) in groupedAttrs.slots"
|
|
312
|
+
:key="name"
|
|
313
|
+
#[name]="scoped"
|
|
314
|
+
>
|
|
315
|
+
<SlotComponent :path="path" v-bind="scoped" :component="slot" />
|
|
316
|
+
</template>
|
|
317
|
+
</component>
|
|
318
|
+
</template>
|
|
319
|
+
</VanField>
|
|
320
|
+
</template>
|
|
321
|
+
|
|
322
|
+
<template v-else>
|
|
323
|
+
<component
|
|
324
|
+
:is="is"
|
|
325
|
+
v-if="is"
|
|
326
|
+
v-bind="groupedAttrs.attrs"
|
|
327
|
+
ref="componentRef"
|
|
328
|
+
v-model:[`${groupedAttrs.modelProp}`]="value"
|
|
329
|
+
>
|
|
330
|
+
<template
|
|
331
|
+
v-for="(slot, name) in groupedAttrs.slots"
|
|
332
|
+
:key="name"
|
|
333
|
+
#[name]="scoped"
|
|
334
|
+
>
|
|
335
|
+
<SlotComponent :path="path" v-bind="scoped" :component="slot" />
|
|
336
|
+
</template>
|
|
337
|
+
</component>
|
|
338
|
+
</template>
|
|
339
|
+
</ContainerFragment>
|
|
340
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Field } from '../../../types';
|
|
2
|
+
import { getInjectConfig } from '../../../../component-provider/constants';
|
|
3
|
+
import { inject } from 'vue';
|
|
4
|
+
|
|
5
|
+
export const getInitProps = (field: Field): Record<string, any> => {
|
|
6
|
+
const { component } = field as any;
|
|
7
|
+
if (typeof component === 'string') {
|
|
8
|
+
const config = getInjectConfig(component);
|
|
9
|
+
if (!config) return {};
|
|
10
|
+
const injectProps = inject(config.injectionKey, config.default);
|
|
11
|
+
return injectProps;
|
|
12
|
+
}
|
|
13
|
+
return {};
|
|
14
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* @component BaseFormItem
|
|
4
|
+
* @description 表单字段渲染引擎
|
|
5
|
+
*
|
|
6
|
+
* 负责将字段配置数组渲染为实际的表单项,支持:
|
|
7
|
+
* - 字段分组(嵌套 fields 递归渲染)
|
|
8
|
+
* - Field 包装(label、rules、校验等)
|
|
9
|
+
* - 组件绑定(通过 BaseField 渲染具体组件,如 Popup 选择器等)
|
|
10
|
+
* - 插槽透传(label 等 Field 插槽)
|
|
11
|
+
*
|
|
12
|
+
* ## 渲染流程
|
|
13
|
+
*
|
|
14
|
+
* 1. 遍历 fields 数组,过滤 hidden 字段
|
|
15
|
+
* 2. 对每个字段:
|
|
16
|
+
* a. 通过 PathProvider 提供当前字段路径
|
|
17
|
+
* b. 通过 GroupedFieldAttrs 将 field 配置拆分
|
|
18
|
+
* c. 如果字段有 `fields` 子字段 → 递归渲染 BaseFormItem(嵌套表单)
|
|
19
|
+
* d. 否则 → 渲染 BaseField(输入组件)
|
|
20
|
+
* 3. 通过 ref 回调收集 Field 实例,注入到 field 对象上
|
|
21
|
+
*
|
|
22
|
+
* @param {Fields<D>} [fields] - 字段配置数组
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
import { computed, toValue } from 'vue';
|
|
27
|
+
import { toPath } from '../../../../shared/core';
|
|
28
|
+
import type { Fields } from '../../types';
|
|
29
|
+
import {
|
|
30
|
+
BaseField,
|
|
31
|
+
ContainerFragment,
|
|
32
|
+
GroupedFieldAttrs,
|
|
33
|
+
PathProvider,
|
|
34
|
+
} from '..';
|
|
35
|
+
|
|
36
|
+
defineOptions({ name: 'BaseFormItem', inheritAttrs: false });
|
|
37
|
+
|
|
38
|
+
type Props = {
|
|
39
|
+
fields?: Fields;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const props = defineProps<Props>();
|
|
43
|
+
|
|
44
|
+
const validFields = computed(() => {
|
|
45
|
+
const f = toValue(props.fields);
|
|
46
|
+
if (!Array.isArray(f)) return [];
|
|
47
|
+
return f.filter(field => !toValue(field.hidden));
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<template
|
|
53
|
+
v-for="(field, index) in validFields"
|
|
54
|
+
:key="toPath(toValue(field.path || index)).join('.') || index"
|
|
55
|
+
>
|
|
56
|
+
<PathProvider :path="toValue(field.path) as any">
|
|
57
|
+
<template #default="{ path }">
|
|
58
|
+
<GroupedFieldAttrs :field="field">
|
|
59
|
+
<template #default="{ componentProps }">
|
|
60
|
+
<template v-if="field.fields">
|
|
61
|
+
<ContainerFragment
|
|
62
|
+
:component="field.fieldContainer"
|
|
63
|
+
:path="path"
|
|
64
|
+
v-bind="componentProps"
|
|
65
|
+
>
|
|
66
|
+
<BaseFormItem :fields="field.fields" />
|
|
67
|
+
</ContainerFragment>
|
|
68
|
+
</template>
|
|
69
|
+
<template v-else>
|
|
70
|
+
<BaseField v-bind="componentProps" :path="path" />
|
|
71
|
+
</template>
|
|
72
|
+
</template>
|
|
73
|
+
</GroupedFieldAttrs>
|
|
74
|
+
</template>
|
|
75
|
+
</PathProvider>
|
|
76
|
+
</template>
|
|
77
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* @component ContainerFragment
|
|
4
|
+
* @description 容器分片渲染组件。用于动态渲染表单行或表单项的外部包裹容器(例如 Grid 行、Col 列等布局容器)。
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
import type { ContainerComponent } from '../../types';
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
component?: ContainerComponent;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
defineProps<Props>();
|
|
15
|
+
|
|
16
|
+
defineOptions({ name: 'ContainerFragment', inheritAttrs: false });
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<component :is="component" v-if="component" v-bind="$attrs">
|
|
21
|
+
<slot />
|
|
22
|
+
</component>
|
|
23
|
+
<slot v-else />
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* @component GroupedFieldAttrs
|
|
4
|
+
* @description 字段属性分组器,将 field 配置对象拆分为多类属性
|
|
5
|
+
*
|
|
6
|
+
* ## 属性分发逻辑
|
|
7
|
+
*
|
|
8
|
+
* 单个 field 对象包含多种属性,需要分发到不同的渲染目标:
|
|
9
|
+
*
|
|
10
|
+
* 1. **fieldAttrs** — 字段包装层属性(label、rules、required 等),传递给 VanField
|
|
11
|
+
* 2. **componentProps** — 组件属性(placeholder、clearable 等),传递给内部组件
|
|
12
|
+
* 3. **fieldSlots** — VanField 的具名插槽
|
|
13
|
+
*
|
|
14
|
+
* 分组依据:
|
|
15
|
+
* - 匹配 vant Field 的 props key → fieldAttrs
|
|
16
|
+
* - 其余所有属性 → componentProps
|
|
17
|
+
* - 属于 FIELD_SLOT_KEYS 的插槽 → fieldSlots
|
|
18
|
+
* - 其余插槽 → componentProps.slots
|
|
19
|
+
*/
|
|
20
|
+
import { computed } from 'vue';
|
|
21
|
+
import type { Field } from '../../types';
|
|
22
|
+
import { isPlainObject } from '../../../../shared/core';
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
field: Field;
|
|
26
|
+
};
|
|
27
|
+
const props = defineProps<Props>();
|
|
28
|
+
|
|
29
|
+
type GroupedFieldAttrs = {
|
|
30
|
+
componentProps: Record<string, any>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// 将 field 配置按 key 分发到各属性分组中
|
|
34
|
+
const groupedAttributes = computed<GroupedFieldAttrs>(() => {
|
|
35
|
+
let componentProps: Record<string, any> = {};
|
|
36
|
+
|
|
37
|
+
if (isPlainObject(props.field)) {
|
|
38
|
+
// prettier-ignore
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
40
|
+
const { path, formItemContainer, fields, hidden, extraProps, ...rest } =
|
|
41
|
+
props.field as any;
|
|
42
|
+
componentProps = rest;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { componentProps };
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<slot v-bind="groupedAttributes"></slot>
|
|
51
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computed, provide } from 'vue';
|
|
3
|
+
import { InjectionPathKey } from '../../constants';
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{ path?: string | number | (string | number)[] }>();
|
|
6
|
+
|
|
7
|
+
const namePath = computed(() => {
|
|
8
|
+
return Array.isArray(props.path)
|
|
9
|
+
? props.path.join('.')
|
|
10
|
+
: (props.path as string | undefined);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
provide(InjectionPathKey, namePath);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<slot :path="namePath" />
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script lang="ts" setup generic="F extends Form<any> = Form">
|
|
2
|
+
/**
|
|
3
|
+
* @component ProForm
|
|
4
|
+
* @description @qin-ui/vant-pro 配置驱动表单组件(移动端)
|
|
5
|
+
*
|
|
6
|
+
* 架构设计说明 (Architecture Overview)
|
|
7
|
+
*
|
|
8
|
+
* [ 数据层 (Core) ] => [ 绑定层 (ProForm) ] => [ 渲染层 (BaseFormItem / BaseField) ]
|
|
9
|
+
*
|
|
10
|
+
* 1. 数据隔离:ProForm 本身不负责存储和管理表单状态,所有核心状态(formData, fields, 校验规则)
|
|
11
|
+
* 均交由底层的 `useForm`(@qin-ui/pro-components-core)驱动。
|
|
12
|
+
* 2. 上下文透传:通过 `provide` 将核心状态下发给子组件,避免了深层嵌套的 Props 传递。
|
|
13
|
+
* 3. 递归渲染:具体字段的解析交由 `<BaseFormItem>` 递归完成,实现了 UI 布局与表单逻辑的解耦。
|
|
14
|
+
*
|
|
15
|
+
* 通过配置驱动的方式快速构建移动端表单,支持:
|
|
16
|
+
* - 字段联动、嵌套字段、自定义组件
|
|
17
|
+
* - 弹出层表单(Popup,适用于移动端选择器场景)
|
|
18
|
+
*
|
|
19
|
+
* @template F - 表单实例类型
|
|
20
|
+
* @param {Form<D>} [form] - useForm 返回的表单实例
|
|
21
|
+
*
|
|
22
|
+
* @slot default - 默认插槽
|
|
23
|
+
* @slot [path] - 以字段 path 命名的动态插槽
|
|
24
|
+
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
import { Form as VanForm, Popup } from '../../../../shared/ui';
|
|
28
|
+
import {
|
|
29
|
+
inject,
|
|
30
|
+
mergeProps,
|
|
31
|
+
provide,
|
|
32
|
+
type Slot,
|
|
33
|
+
watchEffect,
|
|
34
|
+
toValue,
|
|
35
|
+
computed,
|
|
36
|
+
} from 'vue';
|
|
37
|
+
import { INJECT_CONFIG } from '../../../component-provider/constants';
|
|
38
|
+
import { BaseFormItem } from '..';
|
|
39
|
+
import { InjectionFormKey, TeleportComponentNamePrefix } from '../../constants';
|
|
40
|
+
import type { VModelProps, PathProps } from '../../types';
|
|
41
|
+
import { Path, camelizeProperties } from '../../../../shared/core';
|
|
42
|
+
import type { Form } from '../../hooks/useForm';
|
|
43
|
+
import { useForm } from '../../hooks/useForm';
|
|
44
|
+
|
|
45
|
+
defineOptions({ name: 'ProForm', inheritAttrs: false });
|
|
46
|
+
|
|
47
|
+
type Props = { form?: F };
|
|
48
|
+
const props = defineProps<Props>();
|
|
49
|
+
|
|
50
|
+
const form = props.form || (useForm(true) as F);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 依赖注入中心:
|
|
54
|
+
* 将 Core 包生成的 Form 实例注入到 Vue 的 Context 中。
|
|
55
|
+
* 供深层子组件(如 BaseFormItem, BaseField, 自定义插槽)跨层级直接访问数据和方法。
|
|
56
|
+
*/
|
|
57
|
+
provide(InjectionFormKey, form as F);
|
|
58
|
+
|
|
59
|
+
const { fields, setFormRef, formPopup } = form as F;
|
|
60
|
+
|
|
61
|
+
const formFields = computed(() => {
|
|
62
|
+
const f = toValue(fields);
|
|
63
|
+
return Array.isArray(f) ? f : [];
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const config = INJECT_CONFIG['pro-form'];
|
|
67
|
+
const injectAttrs = inject(config.injectionKey, config.default);
|
|
68
|
+
|
|
69
|
+
type ExtractPath<T> = T extends Form<infer D> ? Path<D> : string;
|
|
70
|
+
type FieldSlotProps = VModelProps &
|
|
71
|
+
PathProps & { disabled?: boolean; [x: string]: any };
|
|
72
|
+
type FieldSlots = Record<ExtractPath<F>, Slot<FieldSlotProps>>;
|
|
73
|
+
|
|
74
|
+
const slots = defineSlots<Partial<FieldSlots & { default: Slot }>>();
|
|
75
|
+
/**
|
|
76
|
+
* 动态插槽映射中心 (Slot Teleporting):
|
|
77
|
+
* 目的:将 ProForm 上以 Field Path 命名的插槽,动态注册为全局可访问的 Context。
|
|
78
|
+
* 实现:BaseField 组件在渲染对应字段时,会通过 inject 尝试获取对应的 Prefix+Path,
|
|
79
|
+
* 如果存在,则将插槽内容渲染到深层的具体位置,从而突破了组件嵌套层级的限制。
|
|
80
|
+
*/
|
|
81
|
+
watchEffect(() => {
|
|
82
|
+
Object.keys(slots).forEach(name => {
|
|
83
|
+
if (name === 'default') return;
|
|
84
|
+
provide(`${TeleportComponentNamePrefix}${name}`, (slots as any)[name]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const popupProps = computed(() => {
|
|
89
|
+
// 从 ProComponentProvider 获取全局配置,与传入的 props 合并
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
91
|
+
const { container, ...rest } = formPopup.props;
|
|
92
|
+
return rest;
|
|
93
|
+
});
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<VanForm
|
|
98
|
+
:ref="(el: any) => setFormRef?.(el)"
|
|
99
|
+
v-bind="mergeProps(injectAttrs, camelizeProperties($attrs))"
|
|
100
|
+
class="pro-form"
|
|
101
|
+
>
|
|
102
|
+
<BaseFormItem :fields="formFields" />
|
|
103
|
+
|
|
104
|
+
<Popup
|
|
105
|
+
v-bind="popupProps"
|
|
106
|
+
:show="formPopup.visible.value"
|
|
107
|
+
@close="formPopup.close"
|
|
108
|
+
>
|
|
109
|
+
<div id="pro-form-popup-content"></div>
|
|
110
|
+
</Popup>
|
|
111
|
+
|
|
112
|
+
<slot />
|
|
113
|
+
</VanForm>
|
|
114
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* @component SlotComponent
|
|
4
|
+
* @description 插槽渲染辅助组件。用于在表单或表格的自定义插槽中动态渲染外部传入的 VNode、渲染函数或静态字符串。
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
import { isVNode } from 'vue';
|
|
9
|
+
import { SlotComponentType } from '../../types';
|
|
10
|
+
|
|
11
|
+
defineOptions({ name: 'SlotComponent', inheritAttrs: false });
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
component?: SlotComponentType;
|
|
15
|
+
};
|
|
16
|
+
defineProps<Props>();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<component
|
|
21
|
+
:is="component"
|
|
22
|
+
v-if="isVNode(component) || typeof component === 'function'"
|
|
23
|
+
v-bind="$attrs"
|
|
24
|
+
/>
|
|
25
|
+
<template v-else>{{ component }}</template>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ComponentProps, ComponentExposed } from 'vue-component-type-helpers';
|
|
2
|
+
import ProForm from './ProForm/index.vue';
|
|
3
|
+
|
|
4
|
+
export { default as BaseField } from './BaseField/index.vue';
|
|
5
|
+
export { default as ContainerFragment } from './ContainerFragment/index.vue';
|
|
6
|
+
export { default as SlotComponent } from './SlotComponent/index.vue';
|
|
7
|
+
export { default as PathProvider } from './PathProvider/index.vue';
|
|
8
|
+
export { default as GroupedFieldAttrs } from './GroupedFieldAttrs/index.vue';
|
|
9
|
+
export { default as BaseFormItem } from './BaseFormItem/index.vue';
|
|
10
|
+
|
|
11
|
+
export { ProForm };
|
|
12
|
+
|
|
13
|
+
// instance
|
|
14
|
+
export type ProFormInstance = ComponentExposed<typeof ProForm>;
|
|
15
|
+
export type ProFormProps = ComponentProps<typeof ProForm>;
|