@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.
Files changed (37) hide show
  1. package/api.json +2 -2
  2. package/es/components/component-provider/components/index.vue.d.ts.map +1 -1
  3. package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -1
  4. package/es/components/form/components/BaseField/utils/index.d.ts +2 -1
  5. package/es/components/form/components/BaseField/utils/index.d.ts.map +1 -1
  6. package/es/components/form/components/BaseFormItem/index.vue.d.ts +60 -9
  7. package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -1
  8. package/es/components/form/components/BaseFormItems/index.vue.d.ts +13 -0
  9. package/es/components/form/components/BaseFormItems/index.vue.d.ts.map +1 -0
  10. package/es/components/form/components/ProForm/index.vue.d.ts.map +1 -1
  11. package/es/components/form/components/index.d.ts +6 -6
  12. package/es/components/form/components/index.d.ts.map +1 -1
  13. package/es/components/form/hooks/useGroupFieldAttrs.d.ts +10 -2
  14. package/es/components/form/hooks/useGroupFieldAttrs.d.ts.map +1 -1
  15. package/es/components/form/utils/fieldKey.d.ts +12 -0
  16. package/es/components/form/utils/fieldKey.d.ts.map +1 -1
  17. package/es/components/table/components/ProTable.vue.d.ts.map +1 -1
  18. package/es/components/table/types/index.d.ts +1 -0
  19. package/es/components/table/types/index.d.ts.map +1 -1
  20. package/es/element-plus-pro.css +8 -7
  21. package/es/index.js +327 -174
  22. package/es/index.js.map +1 -1
  23. package/es/shared/core/index.d.ts +1 -1
  24. package/es/shared/core/index.d.ts.map +1 -1
  25. package/package.json +2 -2
  26. package/src/components/component-provider/components/index.vue +25 -8
  27. package/src/components/form/components/BaseField/index.vue +20 -12
  28. package/src/components/form/components/BaseField/utils/index.ts +8 -3
  29. package/src/components/form/components/BaseFormItem/index.vue +145 -176
  30. package/src/components/form/components/BaseFormItems/index.vue +183 -0
  31. package/src/components/form/components/ProForm/index.vue +13 -6
  32. package/src/components/form/components/index.ts +6 -6
  33. package/src/components/form/hooks/useGroupFieldAttrs.ts +104 -79
  34. package/src/components/form/utils/fieldKey.ts +64 -0
  35. package/src/components/table/components/ProTable.vue +11 -5
  36. package/src/components/table/types/index.ts +1 -0
  37. package/src/shared/core/index.ts +2 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * 统一 re-export @qin-ui/pro-components-core,包内所有文件通过此文件引入 core 内容。
3
3
  */
4
- export { type AdditionalMethods, localePacks, normalizeColumn, normalizeColumns, useForm, createFormProxy, useTable, useSearchFormExpand, useFields, resolveFieldIdentity, DEFAULT_FIELD_IDENTITY_KEYS, useFormData, useFormRef, useReactiveAttrs, InjectionFormKey, InjectionFormSlotsKey, getObject, omitBy, get, set, normalizePathKey, toPath, camelizeProperties, cloneDeep, pick, omit, isPlainObject, isObjectItem, isPromise, zhCN, zhTW, enUS, format, resolveLocaleName, type ProLocale, type ProLocaleName, type ProLocaleKey, type Data, type Path, type Paths, type KeyExpandString, type ExtendWithAny, type DeepPartial, type PageParam, type Fields, type Form, type Table, type UseFieldsReturn, type UseFormDataReturn, type UseFormRefReturn, } from '@qin-ui/pro-components-core';
4
+ export { type AdditionalMethods, localePacks, normalizeColumn, normalizeColumns, useForm, createFormProxy, useTable, useSearchFormExpand, useFields, resolveFieldIdentity, DEFAULT_FIELD_IDENTITY_KEYS, useFormData, useFormRef, useReactiveAttrs, InjectionFormKey, InjectionFormSlotsKey, getObject, mergeComponentVars, omitBy, get, set, normalizePathKey, toPath, camelizeProperties, cloneDeep, pick, omit, isPlainObject, isObjectItem, isPromise, callListeners, zhCN, zhTW, enUS, format, resolveLocaleName, type ProLocale, type ProLocaleName, type ProLocaleKey, type Data, type Path, type Paths, type KeyExpandString, type ExtendWithAny, type DeepPartial, type PageParam, type Fields, type Form, type Table, type UseFieldsReturn, type UseFormDataReturn, type UseFormRefReturn, } from '@qin-ui/pro-components-core';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/core/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAEL,KAAK,iBAAiB,EACtB,WAAW,EACX,eAAe,EACf,gBAAgB,EAEhB,OAAO,EACP,eAAe,EACf,QAAQ,EACR,mBAAmB,EACnB,SAAS,EACT,oBAAoB,EACpB,2BAA2B,EAC3B,WAAW,EACX,UAAU,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,qBAAqB,EAErB,SAAS,EACT,MAAM,EACN,GAAG,EACH,GAAG,EACH,gBAAgB,EAChB,MAAM,EACN,kBAAkB,EAClB,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,YAAY,EACZ,SAAS,EAET,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,iBAAiB,EACjB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,YAAY,EAEjB,KAAK,IAAI,EACT,KAAK,IAAI,EACT,KAAK,KAAK,EACV,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,IAAI,EACT,KAAK,KAAK,EACV,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/core/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAEL,KAAK,iBAAiB,EACtB,WAAW,EACX,eAAe,EACf,gBAAgB,EAEhB,OAAO,EACP,eAAe,EACf,QAAQ,EACR,mBAAmB,EACnB,SAAS,EACT,oBAAoB,EACpB,2BAA2B,EAC3B,WAAW,EACX,UAAU,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,qBAAqB,EAErB,SAAS,EACT,kBAAkB,EAClB,MAAM,EACN,GAAG,EACH,GAAG,EACH,gBAAgB,EAChB,MAAM,EACN,kBAAkB,EAClB,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,aAAa,EACb,YAAY,EACZ,SAAS,EACT,aAAa,EAEb,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,iBAAiB,EACjB,KAAK,SAAS,EACd,KAAK,aAAa,EAClB,KAAK,YAAY,EAEjB,KAAK,IAAI,EACT,KAAK,IAAI,EACT,KAAK,KAAK,EACV,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,MAAM,EACX,KAAK,IAAI,EACT,KAAK,KAAK,EACV,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,6BAA6B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qin-ui/element-plus-pro",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
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.1"
43
+ "@qin-ui/pro-components-core": "^2.2.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24",
@@ -36,8 +36,8 @@ import {
36
36
  ensureInjectConfig,
37
37
  } from '../constants';
38
38
  import type { ComponentVars } from '../types';
39
- import { computed, provide } from 'vue';
40
- import { getObject, omit } from '../../../shared/core';
39
+ import { computed, inject, provide, toValue } from 'vue';
40
+ import { getObject, mergeComponentVars, omit } from '../../../shared/core';
41
41
  import type { ComponentName } from '../../form/constants';
42
42
  import { LOCALE_INJECT_KEY, type ProLocale } from '../../../locale';
43
43
 
@@ -74,20 +74,22 @@ const props = withDefaults(defineProps<Props>(), {
74
74
  });
75
75
 
76
76
  /*
77
- * 遍历 componentVars 的每个 key,获取或创建对应的 inject 配置
78
- * 将用户提供的值合并到默认值上,剔除该注入项的不可注入键后通过 provide 响应式注入到子组件树
77
+ * 遍历 componentVars 的每个 key,读取祖先注入值与本层配置合并后向下 provide
78
+ * 合并顺序为内置默认值 → 祖先注入值 → 本层 componentVars,剔除该注入项的不可注入键
79
79
  * 子组件通过 inject(config.injectionKey) 获取合并后的计算属性配置
80
80
  */
81
81
  (Object.keys(INJECT_CONFIG) as Array<keyof typeof INJECT_CONFIG>).forEach(
82
82
  key => {
83
83
  const config = INJECT_CONFIG[key];
84
84
  const ignored = config.ignoreProps;
85
+ const parentConfig = inject(config.injectionKey as any, undefined);
85
86
  provide(
86
87
  config.injectionKey as any,
87
88
  computed(() =>
88
89
  omit(
89
90
  {
90
91
  ...config.default,
92
+ ...(toValue(parentConfig) || {}),
91
93
  ...getObject(props.componentVars?.[key as keyof ComponentVars]),
92
94
  },
93
95
  ignored ?? []
@@ -102,10 +104,11 @@ if (props.componentVars) {
102
104
  Object.keys(props.componentVars).forEach(key => {
103
105
  if (!(key in INJECT_CONFIG)) {
104
106
  const config = ensureInjectConfig(key);
107
+ const parentConfig = inject(config.injectionKey as any, undefined);
105
108
  provide(
106
109
  config.injectionKey as any,
107
110
  computed(() => ({
108
- ...config.default,
111
+ ...(toValue(parentConfig) || {}),
109
112
  ...getObject((props.componentVars as any)?.[key]),
110
113
  }))
111
114
  );
@@ -113,22 +116,36 @@ if (props.componentVars) {
113
116
  });
114
117
  }
115
118
 
119
+ const parentComponentVars = inject(INJECT_COMPONENT_VARS, undefined);
116
120
  // 全量 componentVars:自定义组件键在运行期新增后同样可被子组件读取
117
121
  provide(
118
122
  INJECT_COMPONENT_VARS,
119
- computed(() => (props.componentVars as Record<string, any>) ?? {})
123
+ computed(() =>
124
+ mergeComponentVars(
125
+ toValue(parentComponentVars),
126
+ props.componentVars as Record<string, any>
127
+ )
128
+ )
120
129
  );
121
130
 
131
+ const parentComponents = inject(INJECT_COMPONENTS, undefined);
122
132
  // 注入自定义组件映射,BaseField 在解析组件时会优先使用此映射
123
133
  provide(
124
134
  INJECT_COMPONENTS,
125
- computed(() => props.componentMap)
135
+ computed(() => ({
136
+ ...(toValue(parentComponents) || {}),
137
+ ...(props.componentMap ?? {}),
138
+ }))
126
139
  );
127
140
 
141
+ const parentLocale = inject(LOCALE_INJECT_KEY, undefined);
128
142
  // 注入内置文案覆盖,useLocale 在解析文案时会优先使用
129
143
  provide(
130
144
  LOCALE_INJECT_KEY,
131
- computed(() => props.locale)
145
+ computed(() => ({
146
+ ...(toValue(parentLocale) || {}),
147
+ ...(props.locale ?? {}),
148
+ }))
132
149
  );
133
150
  </script>
134
151
 
@@ -40,6 +40,7 @@ import {
40
40
  import {
41
41
  cloneDeep,
42
42
  getObject,
43
+ omitBy,
43
44
  useReactiveAttrs,
44
45
  } from '../../../../shared/core';
45
46
  import { useDisabledContext } from '../../../../shared/ui';
@@ -81,20 +82,23 @@ const componentRef = ref<any>();
81
82
  const attrs: any = useReactiveAttrs();
82
83
 
83
84
  /**
84
- * 全局默认配置注入:key = 组件名 + type 修饰段,inject 须在 setup 同步期调用。
85
+ * 全局默认配置注入:按组件名与 type 修饰段分别解析注入通道,inject 须在 setup 同步期调用。
85
86
  * 组件名与 `type` 仅取 setup 期初始值——运行期变更不会切换 Provider 的默认值预设
86
87
  * (`date-picker.month` 等子类型预设需在初始渲染前确定)。
88
+ * `date-picker` 的 type 缺省时按 `date` 段解析子类型预设。
87
89
  */
88
90
  const typeVal = attrs.type;
89
- const injectConfigKey =
90
- typeof props.component === 'string'
91
- ? [props.component, typeVal].filter(Boolean).join('.')
92
- : '';
93
- const componentInjectConfig = injectConfigKey
94
- ? getInjectConfig(injectConfigKey)
91
+ const baseConfigKey =
92
+ typeof props.component === 'string' ? props.component : '';
93
+ const typeConfigKey =
94
+ typeVal || (baseConfigKey === 'date-picker' ? 'date' : '');
95
+ const subtypeConfigKey =
96
+ baseConfigKey && typeConfigKey ? `${baseConfigKey}.${typeConfigKey}` : '';
97
+ const baseInjectConfig = baseConfigKey
98
+ ? getInjectConfig(baseConfigKey)
95
99
  : undefined;
96
- const rawInjectComponentProps = componentInjectConfig
97
- ? inject(componentInjectConfig.injectionKey, componentInjectConfig.default)
100
+ const subtypeInjectConfig = subtypeConfigKey
101
+ ? getInjectConfig(subtypeConfigKey)
98
102
  : undefined;
99
103
  // 全量 componentVars:运行期新增的自定义组件键由此兜底读取
100
104
  const componentVars = inject(INJECT_COMPONENT_VARS, undefined);
@@ -149,9 +153,12 @@ const parentDisabled = useDisabledContext();
149
153
  * 属性分为三类:绑定属性(v-model)、组件属性(传递给组件)、插槽(动态渲染)
150
154
  */
151
155
  const groupedAttrs = computed(() => {
156
+ // 库内置默认(基础键 → 子类型键)之后再铺项目配置,项目配置始终覆盖库默认
152
157
  const currentInjectProps = {
153
- ...(toValue(rawInjectComponentProps) || {}),
154
- ...getObject(toValue(componentVars)?.[injectConfigKey]),
158
+ ...getObject(baseInjectConfig?.default),
159
+ ...getObject(subtypeInjectConfig?.default),
160
+ ...getObject(toValue(componentVars)?.[baseConfigKey]),
161
+ ...getObject(toValue(componentVars)?.[subtypeConfigKey]),
155
162
  };
156
163
  const initProps = getInitProps(
157
164
  {
@@ -169,7 +176,8 @@ const groupedAttrs = computed(() => {
169
176
  );
170
177
  const mergedProps = mergeProps(
171
178
  initProps,
172
- attrs,
179
+ // 未赋值(undefined)的键不作为显式值下发,保留注入默认值与占位符兜底
180
+ omitBy(attrs, value => value === undefined),
173
181
  { class: attrs.componentClass, style: attrs.componentStyle },
174
182
  { disabled: effectiveDisabled },
175
183
  { modelProp: modelPropName }
@@ -65,7 +65,8 @@ const getPlaceholderProps = (
65
65
  /**
66
66
  * 组件初始属性:以内置默认值为底,按下发组件名补默认占位符文案
67
67
  *
68
- * @description 组件名由 `component` 与 `type` 拼接(如 `input.password`);`component` 非字符串时返回空记录。
68
+ * @description 占位符按 `component` 与 `type` 拼接的键查表(如 `input.password`),
69
+ * 该键无占位符映射时回落到基础组件名;`component` 非字符串时返回空记录。
69
70
  * @param {Field} field - 字段配置
70
71
  * @param {Record<string, any>} injectProps - Provider 下发的组件默认属性
71
72
  * @param {LocaleT} t - 取文案的翻译函数
@@ -78,8 +79,12 @@ export const getInitProps = (
78
79
  ): Record<string, any> => {
79
80
  const { component, type } = field as any;
80
81
  if (typeof component === 'string') {
81
- const k = [component, type].filter(Boolean).join('.');
82
- return { ...injectProps, ...getPlaceholderProps(k, injectProps, t) };
82
+ const subtypeKey = [component, type].filter(Boolean).join('.');
83
+ const key =
84
+ PLACEHOLDER_KEYS[subtypeKey] || RANGE_PLACEHOLDER_KEYS[subtypeKey]
85
+ ? subtypeKey
86
+ : component;
87
+ return { ...injectProps, ...getPlaceholderProps(key, injectProps, t) };
83
88
  }
84
89
  return {};
85
90
  };
@@ -1,205 +1,174 @@
1
- <script lang="ts" setup>
1
+ <script lang="ts">
2
2
  /**
3
- * 表单字段渲染引擎
3
+ * 单字段行渲染组件
4
4
  *
5
5
  * @component BaseFormItem
6
6
  *
7
- * 负责将字段配置数组渲染为实际的表单项,支持:
8
- * - 字段分组(嵌套 fields 递归渲染,嵌套字段 path 须为完整点分路径)
9
- * - 网格布局(grid 属性控制)
10
- * - FormItem 包装(label、rules、校验等)
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
- getObject,
29
- isObjectItem,
30
- normalizePathKey,
31
- } from '../../../../shared/core';
32
- import type { GridProps } from '../../../../shared/ui';
33
- import {
34
- FormItem,
35
- Grid as ElGrid,
36
- GridItem as ElGridItem,
37
- useProviderDisabled,
38
- } from '../../../../shared/ui';
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 ContainerFragment from '../ContainerFragment/index.vue';
44
- import { useGroupFieldAttrs } from '../../hooks/useGroupFieldAttrs';
45
- import { INJECT_CONFIG } from '../../../component-provider/constants';
35
+ import {
36
+ useGroupedField,
37
+ type GroupedField,
38
+ } from '../../hooks/useGroupFieldAttrs';
46
39
 
47
- defineOptions({ name: 'BaseFormItem', inheritAttrs: false });
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 Props = {
50
- grid?: Grid;
51
- fields?: Fields;
52
- disabled?: boolean;
53
- };
54
- const props = withDefaults(defineProps<Props>(), {
55
- grid: undefined,
56
- fields: () => [],
57
- disabled: undefined,
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
- // 分组字段的禁用状态向下注入,供 BaseField 经禁用上下文消费
61
- useProviderDisabled(computed(() => props.disabled));
81
+ const setItemRef = (el: any) => {
82
+ props.setFormItemRef?.(
83
+ props.field,
84
+ el,
85
+ () => grouped.value.formItemProps
86
+ );
87
+ };
62
88
 
63
- const validFields = computed(() =>
64
- // hidden 支持 Ref / ComputedRef 响应式形态(统一使用 toValue 取值)
65
- (props.fields ?? []).filter(
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
- const config = INJECT_CONFIG['pro-form'];
71
- const injectFormConfig = inject(config.injectionKey, config.default);
72
- const injectGrid = computed(() => toValue(injectFormConfig)?.grid);
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
- const enableGrid = computed(() => !!props.grid);
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
- const computedGridProps = computed<GridProps>(() => {
77
- return enableGrid.value
78
- ? { ...getObject(injectGrid.value), ...getObject(props.grid) }
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
- * 以字段原始对象为 key 登记 FormItem / 控件实例,供 getFormItemRef / getComponentRef 读取;
84
- * 字段对象被回收后对应条目不再可读。
85
- */
86
- const formItemInstanceMap = new WeakMap<object, any>();
87
- const componentInstanceMap = new WeakMap<object, any>();
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 warnedFrozenFields = new WeakSet<object>();
91
- const defineFieldAccessors = (
92
- raw: object,
93
- accessors: Record<string, () => any>
94
- ) => {
95
- try {
96
- for (const [key, getter] of Object.entries(accessors)) {
97
- // 访问器仅用于字段对象读取,不参与属性遍历与字段配置透传
98
- Object.defineProperty(raw, key, {
99
- value: getter,
100
- enumerable: false,
101
- configurable: true,
102
- writable: true,
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
- } catch {
106
- if (!warnedFrozenFields.has(raw)) {
107
- warnedFrozenFields.add(raw);
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) ?? props.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
- :disabled="toValue(item.rawField.disabled) ?? props.disabled"
193
- />
194
- </template>
195
- </FormItem>
196
- </ContainerFragment>
197
- </ContainerFragment>
198
- </template>
199
- </ContainerFragment>
200
- </template>
201
-
202
170
  <style scoped lang="less">
171
+ /* 分组字段的嵌套 FormItem 间距 */
203
172
  [class*='-form-item'] [class*='-form-item'] {
204
173
  margin-bottom: 18px;
205
174
  }