@qin-ui/element-plus-pro 1.4.2 → 1.4.4
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/component-provider/components/index.vue.d.ts.map +1 -1
- package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -1
- package/es/components/form/components/BaseField/utils/index.d.ts +2 -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/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.map +1 -1
- package/es/components/table/types/index.d.ts +1 -0
- package/es/components/table/types/index.d.ts.map +1 -1
- package/es/element-plus-pro.css +8 -7
- package/es/index.js +327 -174
- package/es/index.js.map +1 -1
- package/es/shared/core/index.d.ts +1 -1
- package/es/shared/core/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/component-provider/components/index.vue +25 -8
- package/src/components/form/components/BaseField/index.vue +20 -12
- package/src/components/form/components/BaseField/utils/index.ts +8 -3
- package/src/components/form/components/BaseFormItem/index.vue +145 -176
- package/src/components/form/components/BaseFormItems/index.vue +183 -0
- package/src/components/form/components/ProForm/index.vue +13 -6
- package/src/components/form/components/index.ts +6 -6
- package/src/components/form/hooks/useGroupFieldAttrs.ts +104 -79
- package/src/components/form/utils/fieldKey.ts +64 -0
- package/src/components/table/components/ProTable.vue +11 -5
- package/src/components/table/types/index.ts +1 -0
- package/src/shared/core/index.ts +2 -0
|
@@ -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
|
*
|
|
@@ -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';
|
|
@@ -105,7 +105,14 @@ const fields = computed(() => {
|
|
|
105
105
|
return (Array.isArray(f) ? f : f?.value) ?? [];
|
|
106
106
|
});
|
|
107
107
|
const formData = computed(() => currentForm.value?.formData);
|
|
108
|
-
|
|
108
|
+
/*
|
|
109
|
+
* ref 回调按渲染期实例路由:卸载回调(null)写回该子树所属表单,
|
|
110
|
+
* 运行期替换 :form 后旧实例的 formRef 同步清空
|
|
111
|
+
*/
|
|
112
|
+
const formRefSetter = computed(() => {
|
|
113
|
+
const form = currentForm.value;
|
|
114
|
+
return (el: any) => form?.setFormRef?.(el);
|
|
115
|
+
});
|
|
109
116
|
|
|
110
117
|
/*
|
|
111
118
|
* formKey 子树重挂载契约:以 WeakMap 为每个 form 实例生成自增 key,绑定到原生 Form 的 :key
|
|
@@ -172,13 +179,13 @@ provide(InjectionFormSlotsKey, formSlotsRef);
|
|
|
172
179
|
<!-- el-form 渲染原生 form 元素,拦截回车触发的默认提交 -->
|
|
173
180
|
<ElForm
|
|
174
181
|
:key="formKey"
|
|
175
|
-
:ref="
|
|
182
|
+
:ref="formRefSetter"
|
|
176
183
|
:model="formData"
|
|
177
184
|
v-bind="formAttrs"
|
|
178
185
|
class="pro-form"
|
|
179
186
|
@submit.prevent
|
|
180
187
|
>
|
|
181
|
-
<
|
|
188
|
+
<BaseFormItems
|
|
182
189
|
:fields="fields"
|
|
183
190
|
:grid="props.grid"
|
|
184
191
|
:disabled="formDisabled"
|
|
@@ -3,12 +3,12 @@ import type {
|
|
|
3
3
|
ComponentExposed,
|
|
4
4
|
} from 'vue-component-type-helpers';
|
|
5
5
|
import ProForm from './ProForm/index.vue';
|
|
6
|
-
import
|
|
6
|
+
import BaseFormItems from './BaseFormItems/index.vue';
|
|
7
7
|
|
|
8
8
|
export { default as BaseField } from './BaseField/index.vue';
|
|
9
9
|
export { default as ContainerFragment } from './ContainerFragment/index.vue';
|
|
10
10
|
export { default as SlotComponent } from './SlotComponent/index.vue';
|
|
11
|
-
export { ProForm,
|
|
11
|
+
export { ProForm, BaseFormItems };
|
|
12
12
|
|
|
13
13
|
// instance
|
|
14
14
|
/** ProForm 组件实例 */
|
|
@@ -16,7 +16,7 @@ export type ProFormInstance = ComponentExposed<typeof ProForm>;
|
|
|
16
16
|
/** ProForm 组件属性 */
|
|
17
17
|
export type ProFormProps = ComponentProps<typeof ProForm>;
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
export type
|
|
21
|
-
/**
|
|
22
|
-
export type
|
|
19
|
+
/** BaseFormItems 组件实例(字段级 FormItem) */
|
|
20
|
+
export type ProFormItemsInstance = ComponentExposed<typeof BaseFormItems>;
|
|
21
|
+
/** BaseFormItems 组件属性(字段级 FormItem) */
|
|
22
|
+
export type ProFormItemsProps = ComponentProps<typeof BaseFormItems>;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
mergeProps,
|
|
5
|
+
toValue,
|
|
6
|
+
type ComputedRef,
|
|
7
|
+
type MaybeRefOrGetter,
|
|
8
|
+
} from 'vue';
|
|
3
9
|
import {
|
|
4
10
|
formItemProps,
|
|
5
11
|
gridItemProps,
|
|
@@ -26,9 +32,87 @@ export interface GroupedField {
|
|
|
26
32
|
componentProps: Record<string, any>;
|
|
27
33
|
container?: Base['formItemContainer'];
|
|
28
34
|
rawField: Field;
|
|
29
|
-
renderKey: string | number;
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/** 将单个字段配置拆分为 grid / formItem / component / slots 分组 */
|
|
38
|
+
const groupField = (
|
|
39
|
+
field: Field,
|
|
40
|
+
rawInjectProps: Record<string, any>
|
|
41
|
+
): GroupedField => {
|
|
42
|
+
let gridItemProps: Record<string, any> = {};
|
|
43
|
+
let formItemProps: Record<string, any> = {};
|
|
44
|
+
const componentProps: Record<string, any> = { slots: {} };
|
|
45
|
+
const formItemSlots: Record<string, any> = {};
|
|
46
|
+
|
|
47
|
+
if (isPlainObject(field)) {
|
|
48
|
+
// prettier-ignore
|
|
49
|
+
const { path: _path, prop: _prop, fields, formItemClass, formItemStyle, formItemContainer, hidden, span, getFormItemRef, getComponentRef, getFormItemComputedProps, getComponentComputedProps, slots = {}, formItemDataAttrs = {}, componentDataAttrs = {}, extraProps, ...rest } =
|
|
50
|
+
field as any;
|
|
51
|
+
|
|
52
|
+
// prettier-ignore
|
|
53
|
+
const { class: injectClassName, style: injectStyle, span: injectSpan, formItemContainer: injectFormItemContainer, xs, sm, md, lg, xl, ...injectRest } = rawInjectProps;
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* 字段显式指定 span 时整体接管栅格,不再叠加注入的响应式断点;
|
|
57
|
+
* 未指定 span 时使用注入的断点配置
|
|
58
|
+
*/
|
|
59
|
+
const initGridItemProps =
|
|
60
|
+
span || span === 0 ? { span } : { xs, sm, md, lg, xl, span: injectSpan };
|
|
61
|
+
|
|
62
|
+
// 分组字段未指定 span 时整行渲染(span 兜底 24),注入的响应式断点保持生效
|
|
63
|
+
gridItemProps = fields
|
|
64
|
+
? { ...initGridItemProps, span: span ?? 24 }
|
|
65
|
+
: initGridItemProps;
|
|
66
|
+
formItemProps = mergeProps(
|
|
67
|
+
injectRest as any,
|
|
68
|
+
{ class: injectClassName, style: injectStyle },
|
|
69
|
+
{ class: toValue(formItemClass), style: toValue(formItemStyle) },
|
|
70
|
+
formItemDataAttrs
|
|
71
|
+
);
|
|
72
|
+
// 字段级容器优先,回退注入级配置
|
|
73
|
+
const container = formItemContainer ?? injectFormItemContainer;
|
|
74
|
+
|
|
75
|
+
Object.keys(rest).forEach(k => {
|
|
76
|
+
if (gridItemPropKeySet.has(k)) {
|
|
77
|
+
gridItemProps[k] = rest[k];
|
|
78
|
+
} else if (formItemPropKeySet.has(k)) {
|
|
79
|
+
(formItemProps as any)[k] = rest[k];
|
|
80
|
+
} else {
|
|
81
|
+
componentProps[k] = rest[k];
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
Object.assign(componentProps, componentDataAttrs);
|
|
86
|
+
Object.keys(slots).forEach(k => {
|
|
87
|
+
if (formItemSlotKeySet.has(k)) {
|
|
88
|
+
formItemSlots[k] = slots[k];
|
|
89
|
+
} else {
|
|
90
|
+
componentProps.slots[k] = slots[k];
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// 字段标识经统一解析(prop 优先,path 兜底);name 作为控件自身属性经 rest 透传
|
|
95
|
+
return {
|
|
96
|
+
path: resolveFieldIdentity(field, ELEMENT_FIELD_IDENTITY_KEYS),
|
|
97
|
+
gridItemProps: gridItemProps as GridItemProps,
|
|
98
|
+
formItemProps: formItemProps as any,
|
|
99
|
+
formItemSlots,
|
|
100
|
+
componentProps,
|
|
101
|
+
container,
|
|
102
|
+
rawField: field,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
path: resolveFieldIdentity(field, ELEMENT_FIELD_IDENTITY_KEYS),
|
|
108
|
+
gridItemProps: gridItemProps as GridItemProps,
|
|
109
|
+
formItemProps: formItemProps as any,
|
|
110
|
+
formItemSlots,
|
|
111
|
+
componentProps,
|
|
112
|
+
rawField: field,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
32
116
|
/**
|
|
33
117
|
* 将 fields 配置数组逐个拆分为 grid / formItem / component / slots 分组。
|
|
34
118
|
*
|
|
@@ -45,82 +129,23 @@ export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
|
|
|
45
129
|
|
|
46
130
|
return computed<GroupedField[]>(() => {
|
|
47
131
|
const rawInjectProps = toValue(injectFormItemProps) || {};
|
|
48
|
-
return validFields.value.map(field =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const componentProps: Record<string, any> = { slots: {} };
|
|
52
|
-
const formItemSlots: Record<string, any> = {};
|
|
53
|
-
|
|
54
|
-
if (isPlainObject(field)) {
|
|
55
|
-
// prettier-ignore
|
|
56
|
-
const { path: _path, prop: _prop, fields, formItemClass, formItemStyle, formItemContainer, hidden, span, getFormItemRef, getComponentRef, getFormItemComputedProps, getComponentComputedProps, slots = {}, formItemDataAttrs = {}, componentDataAttrs = {}, extraProps, ...rest } =
|
|
57
|
-
field as any;
|
|
58
|
-
|
|
59
|
-
// prettier-ignore
|
|
60
|
-
const { class: injectClassName, style: injectStyle, span: injectSpan, formItemContainer: injectFormItemContainer, xs, sm, md, lg, xl, ...injectRest } = rawInjectProps;
|
|
61
|
-
|
|
62
|
-
/*
|
|
63
|
-
* 字段显式指定 span 时整体接管栅格,不再叠加注入的响应式断点;
|
|
64
|
-
* 未指定 span 时使用注入的断点配置
|
|
65
|
-
*/
|
|
66
|
-
const initGridItemProps =
|
|
67
|
-
span || span === 0
|
|
68
|
-
? { span }
|
|
69
|
-
: { xs, sm, md, lg, xl, span: injectSpan };
|
|
70
|
-
|
|
71
|
-
// 分组字段未指定 span 时整行渲染(span 兜底 24),注入的响应式断点保持生效
|
|
72
|
-
gridItemProps = fields
|
|
73
|
-
? { ...initGridItemProps, span: span ?? 24 }
|
|
74
|
-
: initGridItemProps;
|
|
75
|
-
formItemProps = mergeProps(
|
|
76
|
-
injectRest as any,
|
|
77
|
-
{ class: injectClassName, style: injectStyle },
|
|
78
|
-
{ class: toValue(formItemClass), style: toValue(formItemStyle) },
|
|
79
|
-
formItemDataAttrs
|
|
80
|
-
);
|
|
81
|
-
const container = formItemContainer ?? injectFormItemContainer;
|
|
82
|
-
|
|
83
|
-
Object.keys(rest).forEach(k => {
|
|
84
|
-
if (gridItemPropKeySet.has(k)) {
|
|
85
|
-
gridItemProps[k] = rest[k];
|
|
86
|
-
} else if (formItemPropKeySet.has(k)) {
|
|
87
|
-
(formItemProps as any)[k] = rest[k];
|
|
88
|
-
} else {
|
|
89
|
-
componentProps[k] = rest[k];
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
Object.assign(componentProps, componentDataAttrs);
|
|
94
|
-
Object.keys(slots).forEach(k => {
|
|
95
|
-
if (formItemSlotKeySet.has(k)) {
|
|
96
|
-
formItemSlots[k] = slots[k];
|
|
97
|
-
} else {
|
|
98
|
-
componentProps.slots[k] = slots[k];
|
|
99
|
-
}
|
|
100
|
-
});
|
|
132
|
+
return validFields.value.map(field => groupField(field, rawInjectProps));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
101
135
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
136
|
+
/**
|
|
137
|
+
* 将单个字段配置拆分为 grid / formItem / component / slots 分组
|
|
138
|
+
*
|
|
139
|
+
* @description 拆分规则与 `useGroupFieldAttrs` 一致,结果依赖该字段自身配置与 Provider 的 `pro-form-item` 默认值。
|
|
140
|
+
*
|
|
141
|
+
* @param {MaybeRefOrGetter<Field>} field - 待拆分的字段配置
|
|
142
|
+
* @returns {ComputedRef<GroupedField>} 单字段渲染分组
|
|
143
|
+
*/
|
|
144
|
+
export function useGroupedField(field: MaybeRefOrGetter<Field>) {
|
|
145
|
+
const config = INJECT_CONFIG['pro-form-item'];
|
|
146
|
+
const injectFormItemProps = inject(config.injectionKey, config.default);
|
|
114
147
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
formItemProps: formItemProps as any,
|
|
119
|
-
formItemSlots,
|
|
120
|
-
componentProps,
|
|
121
|
-
rawField: field,
|
|
122
|
-
renderKey: getFieldKey(field),
|
|
123
|
-
};
|
|
124
|
-
});
|
|
125
|
-
});
|
|
148
|
+
return computed<GroupedField>(() =>
|
|
149
|
+
groupField(toValue(field), toValue(injectFormItemProps) || {})
|
|
150
|
+
);
|
|
126
151
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { toRaw } from 'vue';
|
|
2
|
+
import { resolveFieldIdentity, toPath } from '../../../shared/core';
|
|
3
|
+
import { ELEMENT_FIELD_IDENTITY_KEYS } from '../constants';
|
|
2
4
|
|
|
3
5
|
const fieldKeyMap = new WeakMap<object, number>();
|
|
4
6
|
let fieldKeySeed = 0;
|
|
@@ -25,3 +27,65 @@ export const getFieldKey = (field: unknown): string | number => {
|
|
|
25
27
|
}
|
|
26
28
|
return key;
|
|
27
29
|
};
|
|
30
|
+
|
|
31
|
+
/** 数据行对象身份编号 */
|
|
32
|
+
const rowKeyMap = new WeakMap<object, number>();
|
|
33
|
+
let rowKeySeed = 0;
|
|
34
|
+
|
|
35
|
+
const getRowKey = (row: object) => {
|
|
36
|
+
const raw = toRaw(row);
|
|
37
|
+
let key = rowKeyMap.get(raw);
|
|
38
|
+
if (key === undefined) {
|
|
39
|
+
key = ++rowKeySeed;
|
|
40
|
+
rowKeyMap.set(raw, key);
|
|
41
|
+
}
|
|
42
|
+
return key;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** 数组下标段判定:仅十进制非负整数段按数组下标处理 */
|
|
46
|
+
const isIndexSegment = (segment: string) => /^(?:0|[1-9]\d*)$/.test(segment);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 字段的数据行渲染 key
|
|
50
|
+
*
|
|
51
|
+
* @description 沿字段路径逐段解析原始表单数据,取路径经过的对象数组行元素身份与行内剩余路径组成 key;
|
|
52
|
+
* 同一数据行对象在数组增删 / 重排后恒得同一 key。路径不经过对象数组行、行元素为原始值或数据缺失时,
|
|
53
|
+
* 回退到 {@link getFieldKey} 的字段对象身份。同 key 的去重由调用方按兄弟列表收敛。
|
|
54
|
+
*
|
|
55
|
+
* @param field - 字段配置项
|
|
56
|
+
* @param rawFormData - 原始表单数据(非响应式对象)
|
|
57
|
+
* @returns 数据行 key;无法定位数据行时返回字段对象身份 key
|
|
58
|
+
*/
|
|
59
|
+
export const getFieldDataKey = (
|
|
60
|
+
field: unknown,
|
|
61
|
+
rawFormData: unknown
|
|
62
|
+
): string | number => {
|
|
63
|
+
const path = resolveFieldIdentity(field, ELEMENT_FIELD_IDENTITY_KEYS);
|
|
64
|
+
if (!path || rawFormData === null || rawFormData === undefined) {
|
|
65
|
+
return getFieldKey(field);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const segments = toPath(path);
|
|
69
|
+
let node: any = rawFormData;
|
|
70
|
+
const rowIds: number[] = [];
|
|
71
|
+
let lastRowIndex = -1;
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < segments.length; i++) {
|
|
74
|
+
if (Array.isArray(node)) {
|
|
75
|
+
const segment = segments[i];
|
|
76
|
+
if (!isIndexSegment(segment)) break;
|
|
77
|
+
const element = node[Number(segment)];
|
|
78
|
+
if (element === null || typeof element !== 'object') break;
|
|
79
|
+
rowIds.push(getRowKey(toRaw(element) as object));
|
|
80
|
+
lastRowIndex = i;
|
|
81
|
+
node = element;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (node === null || typeof node !== 'object') break;
|
|
85
|
+
node = node[segments[i]];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (lastRowIndex === -1) return getFieldKey(field);
|
|
89
|
+
const rest = segments.slice(lastRowIndex + 1).join('.');
|
|
90
|
+
return `data:${rowIds.join('/')}:${rest}`;
|
|
91
|
+
};
|
|
@@ -87,6 +87,7 @@ import { useLocale } from '../../../locale';
|
|
|
87
87
|
import type { Path } from '../../../shared/core';
|
|
88
88
|
import {
|
|
89
89
|
camelizeProperties,
|
|
90
|
+
callListeners,
|
|
90
91
|
getObject,
|
|
91
92
|
isObjectItem,
|
|
92
93
|
isPlainObject,
|
|
@@ -243,16 +244,21 @@ const _search = async () => {
|
|
|
243
244
|
if (props.search && attrs.onSearch) {
|
|
244
245
|
warnSearchConfig('duplicate');
|
|
245
246
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
const results = callListeners(search);
|
|
248
|
+
if (!results.length) return;
|
|
249
|
+
const promises = results.filter(isPromise);
|
|
250
|
+
if (!promises.length) {
|
|
249
251
|
warnSearchConfig('not-promise');
|
|
250
252
|
return;
|
|
251
253
|
}
|
|
252
254
|
const seq = ++searchSequence;
|
|
253
255
|
loading.value = true;
|
|
254
256
|
try {
|
|
255
|
-
await
|
|
257
|
+
const settled = await Promise.allSettled(promises);
|
|
258
|
+
const rejected = settled.find(item => item.status === 'rejected') as
|
|
259
|
+
| PromiseRejectedResult
|
|
260
|
+
| undefined;
|
|
261
|
+
if (rejected) throw rejected.reason;
|
|
256
262
|
} finally {
|
|
257
263
|
if (seq === searchSequence) {
|
|
258
264
|
loading.value = false;
|
|
@@ -472,7 +478,7 @@ const onSizeChange = (pageSize: number) => {
|
|
|
472
478
|
|
|
473
479
|
const onPaginationChange = (currentPage: number, pageSize: number) => {
|
|
474
480
|
// 透传 el-pagination change 回调
|
|
475
|
-
attrs.onChange
|
|
481
|
+
callListeners(attrs.onChange, currentPage, pageSize);
|
|
476
482
|
};
|
|
477
483
|
|
|
478
484
|
const searchFormVisible = computed(() => {
|
|
@@ -59,6 +59,7 @@ export type Column<D extends Data = Data> = Partial<
|
|
|
59
59
|
* 列是否隐藏;支持布尔、Ref 与 ComputedRef 形式,渲染期按 `toValue` 取值
|
|
60
60
|
*
|
|
61
61
|
* 分组列的子列同样参与过滤(递归生效)。
|
|
62
|
+
* 只读 ref(如 `computed`)经 `setColumn` 写入时会被普通值替换,可写 ref 保持绑定。
|
|
62
63
|
*/
|
|
63
64
|
hidden?: MaybeRefOrComputedRef<boolean>;
|
|
64
65
|
/** 自定义渲染函数 */
|
package/src/shared/core/index.ts
CHANGED