@qin-ui/element-plus-pro 1.2.4 → 1.3.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.
Files changed (171) hide show
  1. package/README.md +5 -5
  2. package/api.json +1 -25
  3. package/es/__tests__/BaseField.spec.d.ts +2 -0
  4. package/es/__tests__/BaseField.spec.d.ts.map +1 -0
  5. package/es/__tests__/BaseFormItem.spec.d.ts +2 -0
  6. package/es/__tests__/BaseFormItem.spec.d.ts.map +1 -0
  7. package/es/__tests__/ProForm.spec.d.ts +2 -0
  8. package/es/__tests__/ProForm.spec.d.ts.map +1 -0
  9. package/es/__tests__/ProTable.spec.d.ts +2 -0
  10. package/es/__tests__/ProTable.spec.d.ts.map +1 -0
  11. package/es/__tests__/SearchForm.spec.d.ts +2 -0
  12. package/es/__tests__/SearchForm.spec.d.ts.map +1 -0
  13. package/es/__tests__/controls.spec.d.ts +2 -0
  14. package/es/__tests__/controls.spec.d.ts.map +1 -0
  15. package/es/__tests__/helpers/fixtures.d.ts +15 -0
  16. package/es/__tests__/helpers/fixtures.d.ts.map +1 -0
  17. package/es/__tests__/helpers/mount.d.ts +38 -0
  18. package/es/__tests__/helpers/mount.d.ts.map +1 -0
  19. package/es/__tests__/hooks-locale.spec.d.ts +2 -0
  20. package/es/__tests__/hooks-locale.spec.d.ts.map +1 -0
  21. package/es/__tests__/index.spec.d.ts +2 -0
  22. package/es/__tests__/index.spec.d.ts.map +1 -0
  23. package/es/__tests__/integration.spec.d.ts +2 -0
  24. package/es/__tests__/integration.spec.d.ts.map +1 -0
  25. package/es/__tests__/setup.d.ts +14 -0
  26. package/es/__tests__/setup.d.ts.map +1 -0
  27. package/es/element-plus-pro.css +14 -14
  28. package/es/index.js +415 -274
  29. package/es/index.js.map +1 -1
  30. package/es/{components → src/components}/component-provider/components/index.vue.d.ts +5 -1
  31. package/es/src/components/component-provider/components/index.vue.d.ts.map +1 -0
  32. package/es/src/components/component-provider/constants/index.d.ts.map +1 -0
  33. package/es/src/components/component-provider/index.d.ts.map +1 -0
  34. package/es/{components → src/components}/component-provider/types/index.d.ts +9 -5
  35. package/es/src/components/component-provider/types/index.d.ts.map +1 -0
  36. package/es/{components → src/components}/form/components/BaseField/index.vue.d.ts +4 -1
  37. package/es/src/components/form/components/BaseField/index.vue.d.ts.map +1 -0
  38. package/es/src/components/form/components/BaseField/utils/index.d.ts +5 -0
  39. package/es/src/components/form/components/BaseField/utils/index.d.ts.map +1 -0
  40. package/es/{components → src/components}/form/components/BaseFormItem/index.vue.d.ts +4 -2
  41. package/es/src/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -0
  42. package/es/src/components/form/components/ContainerFragment/index.vue.d.ts.map +1 -0
  43. package/es/src/components/form/components/ProForm/index.vue.d.ts.map +1 -0
  44. package/es/src/components/form/components/SlotComponent/index.vue.d.ts.map +1 -0
  45. package/es/src/components/form/components/index.d.ts.map +1 -0
  46. package/es/src/components/form/constants/index.d.ts.map +1 -0
  47. package/es/src/components/form/hooks/index.d.ts.map +1 -0
  48. package/es/src/components/form/hooks/useFields.d.ts.map +1 -0
  49. package/es/src/components/form/hooks/useForm.d.ts.map +1 -0
  50. package/es/src/components/form/hooks/useFormRef.d.ts.map +1 -0
  51. package/es/src/components/form/hooks/useGroupFieldAttrs.d.ts.map +1 -0
  52. package/es/src/components/form/index.d.ts.map +1 -0
  53. package/es/{components → src/components}/form/types/index.d.ts +26 -2
  54. package/es/src/components/form/types/index.d.ts.map +1 -0
  55. package/es/{components → src/components}/table/components/ColumnControl.vue.d.ts +4 -1
  56. package/es/src/components/table/components/ColumnControl.vue.d.ts.map +1 -0
  57. package/es/src/components/table/components/DefaultSearchFormContainer.vue.d.ts.map +1 -0
  58. package/es/src/components/table/components/DefaultTableContainer.vue.d.ts.map +1 -0
  59. package/es/src/components/table/components/ProTable.vue.d.ts.map +1 -0
  60. package/es/{components → src/components}/table/components/SearchForm.vue.d.ts +14 -4
  61. package/es/src/components/table/components/SearchForm.vue.d.ts.map +1 -0
  62. package/es/src/components/table/components/SizeControl.vue.d.ts.map +1 -0
  63. package/es/src/components/table/components/TableColumnRenderer.vue.d.ts.map +1 -0
  64. package/es/src/components/table/components/icons/ColumnHeightOutlined.vue.d.ts.map +1 -0
  65. package/es/src/components/table/components/icons/DownOutlined.vue.d.ts.map +1 -0
  66. package/es/src/components/table/components/icons/FilterOutlined.vue.d.ts.map +1 -0
  67. package/es/src/components/table/components/index.d.ts.map +1 -0
  68. package/es/src/components/table/index.d.ts.map +1 -0
  69. package/es/src/components/table/types/index.d.ts.map +1 -0
  70. package/es/src/components/table/useTable.d.ts.map +1 -0
  71. package/es/{index.d.ts → src/index.d.ts} +1 -1
  72. package/es/src/index.d.ts.map +1 -0
  73. package/es/src/locale/en-US.d.ts.map +1 -0
  74. package/es/src/locale/index.d.ts.map +1 -0
  75. package/es/src/locale/zh-CN.d.ts.map +1 -0
  76. package/es/src/locale/zh-TW.d.ts.map +1 -0
  77. package/es/src/shared/core/index.d.ts +6 -0
  78. package/es/src/shared/core/index.d.ts.map +1 -0
  79. package/es/{shared → src/shared}/ui/index.d.ts +2 -0
  80. package/es/src/shared/ui/index.d.ts.map +1 -0
  81. package/package.json +8 -3
  82. package/src/components/component-provider/components/index.vue +37 -8
  83. package/src/components/component-provider/constants/index.ts +2 -2
  84. package/src/components/component-provider/types/index.ts +9 -5
  85. package/src/components/form/components/BaseField/index.vue +67 -35
  86. package/src/components/form/components/BaseField/utils/index.ts +6 -11
  87. package/src/components/form/components/BaseFormItem/index.vue +47 -41
  88. package/src/components/form/components/ProForm/index.vue +72 -18
  89. package/src/components/form/hooks/useGroupFieldAttrs.ts +8 -4
  90. package/src/components/form/types/index.ts +26 -2
  91. package/src/components/table/components/ColumnControl.vue +18 -24
  92. package/src/components/table/components/ProTable.vue +112 -57
  93. package/src/components/table/components/SearchForm.vue +85 -60
  94. package/src/components/table/components/SizeControl.vue +2 -2
  95. package/src/components/table/components/TableColumnRenderer.vue +12 -9
  96. package/src/components/table/useTable.ts +12 -7
  97. package/src/index.ts +1 -1
  98. package/src/locale/index.ts +6 -2
  99. package/src/shared/core/index.ts +4 -2
  100. package/src/shared/ui/index.ts +11 -5
  101. package/es/components/component-provider/components/index.vue.d.ts.map +0 -1
  102. package/es/components/component-provider/constants/index.d.ts.map +0 -1
  103. package/es/components/component-provider/index.d.ts.map +0 -1
  104. package/es/components/component-provider/types/index.d.ts.map +0 -1
  105. package/es/components/form/components/BaseField/index.vue.d.ts.map +0 -1
  106. package/es/components/form/components/BaseField/utils/index.d.ts +0 -5
  107. package/es/components/form/components/BaseField/utils/index.d.ts.map +0 -1
  108. package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +0 -1
  109. package/es/components/form/components/ContainerFragment/index.vue.d.ts.map +0 -1
  110. package/es/components/form/components/ProForm/index.vue.d.ts.map +0 -1
  111. package/es/components/form/components/SlotComponent/index.vue.d.ts.map +0 -1
  112. package/es/components/form/components/index.d.ts.map +0 -1
  113. package/es/components/form/constants/index.d.ts.map +0 -1
  114. package/es/components/form/hooks/index.d.ts.map +0 -1
  115. package/es/components/form/hooks/useFields.d.ts.map +0 -1
  116. package/es/components/form/hooks/useForm.d.ts.map +0 -1
  117. package/es/components/form/hooks/useFormRef.d.ts.map +0 -1
  118. package/es/components/form/hooks/useGroupFieldAttrs.d.ts.map +0 -1
  119. package/es/components/form/index.d.ts.map +0 -1
  120. package/es/components/form/types/index.d.ts.map +0 -1
  121. package/es/components/table/components/ColumnControl.vue.d.ts.map +0 -1
  122. package/es/components/table/components/DefaultSearchFormContainer.vue.d.ts.map +0 -1
  123. package/es/components/table/components/DefaultTableContainer.vue.d.ts.map +0 -1
  124. package/es/components/table/components/ProTable.vue.d.ts.map +0 -1
  125. package/es/components/table/components/SearchForm.vue.d.ts.map +0 -1
  126. package/es/components/table/components/SizeControl.vue.d.ts.map +0 -1
  127. package/es/components/table/components/TableColumnRenderer.vue.d.ts.map +0 -1
  128. package/es/components/table/components/icons/ColumnHeightOutlined.vue.d.ts.map +0 -1
  129. package/es/components/table/components/icons/DownOutlined.vue.d.ts.map +0 -1
  130. package/es/components/table/components/icons/FilterOutlined.vue.d.ts.map +0 -1
  131. package/es/components/table/components/index.d.ts.map +0 -1
  132. package/es/components/table/index.d.ts.map +0 -1
  133. package/es/components/table/types/index.d.ts.map +0 -1
  134. package/es/components/table/useTable.d.ts.map +0 -1
  135. package/es/index.d.ts.map +0 -1
  136. package/es/locale/en-US.d.ts.map +0 -1
  137. package/es/locale/index.d.ts.map +0 -1
  138. package/es/locale/zh-CN.d.ts.map +0 -1
  139. package/es/locale/zh-TW.d.ts.map +0 -1
  140. package/es/shared/core/index.d.ts +0 -6
  141. package/es/shared/core/index.d.ts.map +0 -1
  142. package/es/shared/ui/index.d.ts.map +0 -1
  143. /package/es/{components → src/components}/component-provider/constants/index.d.ts +0 -0
  144. /package/es/{components → src/components}/component-provider/index.d.ts +0 -0
  145. /package/es/{components → src/components}/form/components/ContainerFragment/index.vue.d.ts +0 -0
  146. /package/es/{components → src/components}/form/components/ProForm/index.vue.d.ts +0 -0
  147. /package/es/{components → src/components}/form/components/SlotComponent/index.vue.d.ts +0 -0
  148. /package/es/{components → src/components}/form/components/index.d.ts +0 -0
  149. /package/es/{components → src/components}/form/constants/index.d.ts +0 -0
  150. /package/es/{components → src/components}/form/hooks/index.d.ts +0 -0
  151. /package/es/{components → src/components}/form/hooks/useFields.d.ts +0 -0
  152. /package/es/{components → src/components}/form/hooks/useForm.d.ts +0 -0
  153. /package/es/{components → src/components}/form/hooks/useFormRef.d.ts +0 -0
  154. /package/es/{components → src/components}/form/hooks/useGroupFieldAttrs.d.ts +0 -0
  155. /package/es/{components → src/components}/form/index.d.ts +0 -0
  156. /package/es/{components → src/components}/table/components/DefaultSearchFormContainer.vue.d.ts +0 -0
  157. /package/es/{components → src/components}/table/components/DefaultTableContainer.vue.d.ts +0 -0
  158. /package/es/{components → src/components}/table/components/ProTable.vue.d.ts +0 -0
  159. /package/es/{components → src/components}/table/components/SizeControl.vue.d.ts +0 -0
  160. /package/es/{components → src/components}/table/components/TableColumnRenderer.vue.d.ts +0 -0
  161. /package/es/{components → src/components}/table/components/icons/ColumnHeightOutlined.vue.d.ts +0 -0
  162. /package/es/{components → src/components}/table/components/icons/DownOutlined.vue.d.ts +0 -0
  163. /package/es/{components → src/components}/table/components/icons/FilterOutlined.vue.d.ts +0 -0
  164. /package/es/{components → src/components}/table/components/index.d.ts +0 -0
  165. /package/es/{components → src/components}/table/index.d.ts +0 -0
  166. /package/es/{components → src/components}/table/types/index.d.ts +0 -0
  167. /package/es/{components → src/components}/table/useTable.d.ts +0 -0
  168. /package/es/{locale → src/locale}/en-US.d.ts +0 -0
  169. /package/es/{locale → src/locale}/index.d.ts +0 -0
  170. /package/es/{locale → src/locale}/zh-CN.d.ts +0 -0
  171. /package/es/{locale → src/locale}/zh-TW.d.ts +0 -0
@@ -33,14 +33,18 @@ import {
33
33
  inject,
34
34
  mergeProps,
35
35
  ref,
36
+ toValue,
36
37
  useAttrs,
37
38
  } from 'vue';
38
- import { cloneDeep, omit } from '../../../../shared/core';
39
+ import { cloneDeep } from '../../../../shared/core';
39
40
  import { useDisabledContext } from '../../../../shared/ui';
40
41
  import type { Field } from '../../types';
41
42
  import { ContainerFragment, SlotComponent } from '..';
42
43
  import { componentMap, InjectionFormSlotsKey } from '../../constants';
43
- import { INJECT_COMPONENTS } from '../../../component-provider/constants';
44
+ import {
45
+ INJECT_COMPONENTS,
46
+ getInjectConfig,
47
+ } from '../../../component-provider/constants';
44
48
  import { useForm } from '../../hooks/useForm';
45
49
  import { getInitProps } from './utils';
46
50
  import { useLocale } from '../../../../locale';
@@ -51,25 +55,36 @@ type Props = {
51
55
  component?: string | Component;
52
56
  path?: string;
53
57
  };
54
- const { component = undefined, path = '' } = defineProps<Props>();
58
+ const props = withDefaults(defineProps<Props>(), {
59
+ component: undefined,
60
+ path: '',
61
+ });
55
62
 
56
63
  const { t } = useLocale();
57
64
 
58
65
  const form = useForm(false);
59
- const { getFormData, setFormData } = form;
60
66
 
61
67
  const componentRef = ref<any>();
62
68
 
63
69
  const attrs: any = useAttrs();
64
70
 
65
- function getOldValue() {
66
- return cloneDeep(getFormData?.(path));
67
- }
71
+ // 全局默认配置注入:key = 组件名 + type 修饰段;inject 须在 setup 同步期调用
72
+ const typeVal = attrs.type;
73
+ const injectConfigKey =
74
+ typeof props.component === 'string'
75
+ ? [props.component, typeVal].filter(Boolean).join('.')
76
+ : '';
77
+ const componentInjectConfig = injectConfigKey
78
+ ? getInjectConfig(injectConfigKey)
79
+ : undefined;
80
+ const rawInjectComponentProps = componentInjectConfig
81
+ ? inject(componentInjectConfig.injectionKey, componentInjectConfig.default)
82
+ : undefined;
68
83
 
69
84
  // setter: 向 formData 写入值,支持 valueFormatter / valueFormatter.set 转换
70
85
  const value = computed({
71
86
  get() {
72
- let val = getFormData?.(path);
87
+ let val = form.getFormData?.(props.path);
73
88
  const { valueFormatter } = groupedAttrs.value;
74
89
  if (
75
90
  typeof valueFormatter === 'object' &&
@@ -82,14 +97,23 @@ const value = computed({
82
97
  set(val) {
83
98
  let newVal = val;
84
99
  const { valueFormatter } = groupedAttrs.value;
85
- if (valueFormatter) {
86
- if (typeof valueFormatter === 'function') {
87
- newVal = valueFormatter(val, getOldValue());
88
- } else if (typeof valueFormatter.set === 'function') {
89
- newVal = valueFormatter.set(val, getOldValue());
90
- }
100
+ const setFormatter =
101
+ typeof valueFormatter === 'function'
102
+ ? valueFormatter
103
+ : typeof valueFormatter === 'object' &&
104
+ typeof valueFormatter?.set === 'function'
105
+ ? valueFormatter.set
106
+ : undefined;
107
+
108
+ if (setFormatter) {
109
+ // 仅当 set 转换函数需要旧值参数时,才获取旧值上下文
110
+ const oldVal =
111
+ setFormatter.length > 1
112
+ ? cloneDeep(form.getFormData?.(props.path))
113
+ : undefined;
114
+ newVal = setFormatter(val, oldVal);
91
115
  }
92
- setFormData?.(path, newVal);
116
+ form.setFormData?.(props.path, newVal);
93
117
  },
94
118
  });
95
119
 
@@ -100,36 +124,38 @@ const parentDisabled = useDisabledContext();
100
124
  * 属性分为三类:绑定属性(v-model)、组件属性(传递给组件)、插槽(动态渲染)
101
125
  */
102
126
  const groupedAttrs = computed(() => {
127
+ const currentInjectProps = toValue(rawInjectComponentProps) || {};
103
128
  const initProps = getInitProps(
104
129
  {
105
- component: component,
130
+ component: props.component,
106
131
  type: attrs.type,
107
132
  } as unknown as Field,
133
+ currentInjectProps,
108
134
  t
109
135
  );
110
136
  const modelPropName = attrs.modelProp ?? initProps.modelProp ?? 'modelValue';
137
+ const effectiveDisabled =
138
+ attrs.disabled !== undefined
139
+ ? Boolean(attrs.disabled)
140
+ : Boolean(parentDisabled.value || initProps.disabled);
111
141
  const mergedProps = mergeProps(
112
142
  initProps,
113
143
  attrs,
114
144
  { class: attrs.componentClass, style: attrs.componentStyle },
115
145
  { class: initProps.componentClass, style: initProps.componentStyle },
116
- { disabled: attrs.disabled ?? parentDisabled.value ?? initProps.disabled },
146
+ { disabled: effectiveDisabled },
117
147
  { modelProp: modelPropName }
118
148
  ) as Required<Field>;
149
+
119
150
  // prettier-ignore
120
151
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
121
- const { valueFormatter, modelProp, slots, componentClass, componentStyle, componentContainer, ...rest } = mergedProps
122
- const modelBindingProp = modelProp ?? 'modelValue';
123
- // 从剩余属性中移除 v-model 绑定属性,避免重复绑定
124
- const bindAttrs = omit(rest as Record<string, any>, [
125
- modelBindingProp,
126
- `onUpdate:${modelBindingProp}`,
127
- ]);
152
+ const { valueFormatter, modelProp, slots, componentClass, componentStyle, componentContainer, [modelPropName]: _modelVal, [`onUpdate:${modelPropName}`]: _onUpdateModelVal, ...bindAttrs } = mergedProps as any;
153
+
128
154
  return {
129
155
  attrs: bindAttrs,
130
156
  slots,
131
157
  componentContainer,
132
- modelProp: modelBindingProp,
158
+ modelProp: modelPropName,
133
159
  valueFormatter,
134
160
  };
135
161
  });
@@ -147,16 +173,19 @@ const customComponents = inject(INJECT_COMPONENTS, {});
147
173
  * ProForm 插槽 → 自定义组件映射 → 内置组件映射 → 原始 component 值
148
174
  */
149
175
  const is = computed(() => {
150
- const teleportSlot = path && formSlots?.[path];
176
+ const teleportSlot = props.path && formSlots?.[props.path];
151
177
  if (teleportSlot) return teleportSlot;
152
- if (typeof component === 'string') {
153
- return (
154
- (customComponents as any)[component] ||
155
- (componentMap as any)[component] ||
156
- component
178
+ if (typeof props.component === 'string') {
179
+ const resolved =
180
+ (customComponents as any)[props.component] ||
181
+ (componentMap as any)[props.component];
182
+ if (resolved) return resolved;
183
+ console.warn(
184
+ `[pro-components] 未识别的组件类型 "${props.component}",已降级为原生元素渲染。如为自定义组件请在 componentMap 或 ProComponentProvider 中注册。`
157
185
  );
186
+ return props.component;
158
187
  }
159
- return component;
188
+ return props.component;
160
189
  });
161
190
 
162
191
  defineExpose({
@@ -166,21 +195,24 @@ defineExpose({
166
195
  </script>
167
196
 
168
197
  <template>
169
- <ContainerFragment :component="groupedAttrs.componentContainer" :path="path">
198
+ <ContainerFragment
199
+ :component="groupedAttrs.componentContainer"
200
+ :path="props.path"
201
+ >
170
202
  <component
171
203
  :is="is"
172
204
  v-if="is"
173
205
  v-bind="groupedAttrs.attrs"
174
206
  ref="componentRef"
175
207
  v-model:[`${groupedAttrs.modelProp}`]="value"
176
- :path="path"
208
+ :path="props.path"
177
209
  >
178
210
  <template
179
211
  v-for="(slot, name) in groupedAttrs.slots"
180
212
  :key="name"
181
213
  #[name]="scoped"
182
214
  >
183
- <SlotComponent :path="path" v-bind="scoped" :component="slot" />
215
+ <SlotComponent :path="props.path" v-bind="scoped" :component="slot" />
184
216
  </template>
185
217
  </component>
186
218
  </ContainerFragment>
@@ -1,6 +1,4 @@
1
1
  import { Field } from '../../../types';
2
- import { getInjectConfig } from '../../../../component-provider/constants';
3
- import { inject } from 'vue';
4
2
 
5
3
  type LocaleT = (
6
4
  key: string,
@@ -38,11 +36,7 @@ const RANGE_PLACEHOLDER_KEYS: Record<string, [string, string]> = {
38
36
  'date-picker.datetimerange': ['date-picker.startTime', 'date-picker.endTime'],
39
37
  };
40
38
 
41
- /**
42
- * 根据组件 key 兜底生成 locale 占位符
43
- *
44
- * @description 用户通过 componentVars 或字段属性显式设置了占位符时不再兜底。
45
- */
39
+ /** 根据组件 key 兜底生成 locale 占位符;已显式设置占位符(componentVars 或字段属性)时不兜底 */
46
40
  const getPlaceholderProps = (
47
41
  key: string,
48
42
  injectProps: Record<string, any>,
@@ -65,13 +59,14 @@ const getPlaceholderProps = (
65
59
  return { placeholder: t(placeholderKey) };
66
60
  };
67
61
 
68
- export const getInitProps = (field: Field, t: LocaleT): Record<string, any> => {
62
+ export const getInitProps = (
63
+ field: Field,
64
+ injectProps: Record<string, any> = {},
65
+ t: LocaleT
66
+ ): Record<string, any> => {
69
67
  const { component, type } = field as any;
70
68
  if (typeof component === 'string') {
71
69
  const k = [component, type].filter(Boolean).join('.');
72
- const config = getInjectConfig(k);
73
- if (!config) return getPlaceholderProps(k, {}, t);
74
- const injectProps = inject(config.injectionKey, config.default);
75
70
  return { ...injectProps, ...getPlaceholderProps(k, injectProps, t) };
76
71
  }
77
72
  return {};
@@ -4,7 +4,7 @@
4
4
  * @description 表单字段渲染引擎
5
5
  *
6
6
  * 负责将字段配置数组渲染为实际的表单项,支持:
7
- * - 字段分组(嵌套 fields 递归渲染)
7
+ * - 字段分组(嵌套 fields 递归渲染,嵌套字段 path 须为完整点分路径)
8
8
  * - 网格布局(grid 属性控制)
9
9
  * - FormItem 包装(label、rules、校验等)
10
10
  * - 组件绑定(通过 BaseField 渲染具体组件)
@@ -19,19 +19,17 @@
19
19
  *
20
20
  * @param {Fields<D>} [fields] - 字段配置数组
21
21
  * @param {boolean | GridProps} [grid] - 是否启用网格布局
22
- * @param {boolean} [disabled] - 是否禁用所有子字段
23
22
  *
24
23
  * @internal
25
24
  */
26
- import { getObject, toPath } from '../../../../shared/core';
25
+ import { getObject, normalizePathKey } from '../../../../shared/core';
27
26
  import {
28
27
  FormItem,
29
28
  Grid as ElGrid,
30
29
  GridItem as ElGridItem,
31
30
  GridProps,
32
- useDisabledContextProvider,
33
31
  } from '../../../../shared/ui';
34
- import { computed, inject, toValue } from 'vue';
32
+ import { computed, inject, toRaw, toValue } from 'vue';
35
33
  import type { Fields, Grid } from '../../types';
36
34
  import { BaseField, SlotComponent, ContainerFragment } from '..';
37
35
  import { useGroupFieldAttrs } from '../../hooks/useGroupFieldAttrs';
@@ -42,51 +40,62 @@ defineOptions({ name: 'BaseFormItem', inheritAttrs: false });
42
40
  type Props = {
43
41
  grid?: Grid;
44
42
  fields?: Fields;
45
- disabled?: boolean;
46
43
  };
47
- const {
48
- grid = undefined,
49
- fields = [],
50
- disabled = undefined,
51
- } = defineProps<Props>();
52
-
53
- const validFields = computed(() => fields.filter?.(field => !field.hidden));
44
+ const props = withDefaults(defineProps<Props>(), {
45
+ grid: undefined,
46
+ fields: () => [],
47
+ });
54
48
 
55
- useDisabledContextProvider(computed(() => disabled) as any);
49
+ const validFields = computed(() =>
50
+ (props.fields ?? []).filter(field => !field.hidden)
51
+ );
56
52
 
57
53
  const config = INJECT_CONFIG['pro-form'];
58
-
59
- const { grid: injectGrid } = inject(config.injectionKey, config.default);
54
+ const injectFormConfig = inject(config.injectionKey, config.default);
55
+ const injectGrid = computed(() => toValue(injectFormConfig)?.grid);
60
56
 
61
57
  const enableGrid = computed(() => {
62
- if (grid !== undefined) return !!grid;
63
- return !!injectGrid;
58
+ if (props.grid !== undefined) return !!props.grid;
59
+ return !!injectGrid.value;
64
60
  });
65
61
 
66
62
  const computedGridProps = computed<GridProps>(() => {
67
63
  return enableGrid.value
68
- ? { ...getObject(injectGrid), ...getObject(grid) }
64
+ ? { ...getObject(injectGrid.value), ...getObject(props.grid) }
69
65
  : {};
70
66
  });
71
67
 
72
- const formItemRefs: any[] = [];
73
- const componentRefs: any[] = [];
68
+ /**
69
+ * 以字段原始对象为 key 登记 FormItem / 控件实例,供 getFormItemRef / getComponentRef 读取;
70
+ * WeakMap 不参与响应式追踪,条目随字段对象回收自动释放。
71
+ */
72
+ const formItemInstanceMap = new WeakMap<object, any>();
73
+ const componentInstanceMap = new WeakMap<object, any>();
74
74
 
75
- const onFormItemMounted = (index: number, formItemProps?: any) => {
76
- const field = validFields.value[index];
77
- Object.assign(field, {
78
- getFormItemRef: () => formItemRefs[index],
79
- getFormItemComputedProps: () => formItemProps,
80
- });
75
+ const setFormItemRef = (field: any, el: any, formItemProps?: any) => {
76
+ if (!field || typeof field !== 'object') return;
77
+ const raw = toRaw(field);
78
+ if (el) {
79
+ formItemInstanceMap.set(raw, el);
80
+ raw.getFormItemRef = () => formItemInstanceMap.get(raw);
81
+ raw.getFormItemComputedProps = () => formItemProps;
82
+ } else {
83
+ formItemInstanceMap.delete(raw);
84
+ }
81
85
  };
82
86
 
83
- const onComponentMounted = (index: number) => {
84
- const field = validFields.value[index];
85
- Object.assign(field, {
86
- getComponentRef: () => componentRefs[index]?.getComponentRef(),
87
- getComponentComputedProps: () =>
88
- componentRefs[index]?.getComponentComputedProps(),
89
- });
87
+ const setComponentRef = (field: any, el: any) => {
88
+ if (!field || typeof field !== 'object') return;
89
+ const raw = toRaw(field);
90
+ if (el) {
91
+ componentInstanceMap.set(raw, el);
92
+ raw.getComponentRef = () =>
93
+ componentInstanceMap.get(raw)?.getComponentRef();
94
+ raw.getComponentComputedProps = () =>
95
+ componentInstanceMap.get(raw)?.getComponentComputedProps();
96
+ } else {
97
+ componentInstanceMap.delete(raw);
98
+ }
90
99
  };
91
100
 
92
101
  const groupedFields = useGroupFieldAttrs(validFields);
@@ -106,10 +115,9 @@ const groupedFields = useGroupFieldAttrs(validFields);
106
115
  <ContainerFragment :component="item.container" :path="item.path">
107
116
  <FormItem
108
117
  v-bind="item.formItemProps"
109
- :ref="el => (formItemRefs[index] = el)"
110
- :prop="toPath(item.path).join('.')"
118
+ :ref="el => setFormItemRef(item.rawField, el, item.formItemProps)"
119
+ :prop="normalizePathKey(item.path)"
111
120
  :path="item.path"
112
- @vue:mounted="onFormItemMounted(index, item.formItemProps)"
113
121
  >
114
122
  <template
115
123
  v-for="(slot, name) in item.formItemSlots"
@@ -124,17 +132,15 @@ const groupedFields = useGroupFieldAttrs(validFields);
124
132
  </template>
125
133
  <template v-if="item.rawField.fields">
126
134
  <BaseFormItem
127
- :grid="(item.rawField as any).grid ?? grid"
135
+ :grid="(item.rawField as any).grid ?? props.grid"
128
136
  :fields="item.rawField.fields"
129
- :disabled="toValue(item.rawField.disabled)"
130
137
  />
131
138
  </template>
132
139
  <template v-else>
133
140
  <BaseField
134
- :ref="el => (componentRefs[index] = el)"
141
+ :ref="el => setComponentRef(item.rawField, el)"
135
142
  v-bind="item.componentProps"
136
143
  :path="item.path"
137
- @vue:mounted="onComponentMounted(index)"
138
144
  />
139
145
  </template>
140
146
  </FormItem>
@@ -32,7 +32,7 @@ import {
32
32
  inject,
33
33
  mergeProps,
34
34
  provide,
35
- reactive,
35
+ toValue,
36
36
  type Slot,
37
37
  computed,
38
38
  useAttrs,
@@ -42,34 +42,87 @@ import { BaseFormItem } from '..';
42
42
  import { InjectionFormKey, InjectionFormSlotsKey } from '../../constants';
43
43
  import type { Grid, VModelProps, PathProps } from '../../types';
44
44
  import { Path, camelizeProperties } from '../../../../shared/core';
45
- import type { Form } from '../../hooks/useForm';
45
+ import { useForm, type Form } from '../../hooks/useForm';
46
46
 
47
47
  defineOptions({ name: 'ProForm', inheritAttrs: false });
48
48
 
49
49
  type FormProps = Partial<Omit<ElFormProps, 'model'>>;
50
50
 
51
- type Props = { grid?: Grid; form?: F } & /* @vue-ignore */ FormProps;
52
- const { grid = false, form = {} as F } = defineProps<Props>();
51
+ type Props = {
52
+ grid?: Grid;
53
+ form?: F;
54
+ } & /* @vue-ignore */ FormProps;
55
+ const props = withDefaults(defineProps<Props>(), {
56
+ grid: false,
57
+ form: undefined,
58
+ });
53
59
 
54
60
  /**
55
61
  * 依赖注入中心:
56
- * 将 Core 包生成的 Form 实例注入到 Vue 的 Context 中。
57
- * 供深层子组件(如 BaseFormItem, BaseField, 自定义插槽)跨层级直接访问数据和方法。
62
+ * 若外部未传 form 实例,则自动初始化内部表单实例;
63
+ * 将 Form 实例注入到 Vue Context 中,供深层子组件跨层级直接访问数据和方法。
58
64
  */
59
- provide(InjectionFormKey, form as F);
65
+ const fallbackForm = props.form ? undefined : useForm();
66
+ const currentForm = computed(
67
+ () => props.form ?? (fallbackForm as unknown as F)
68
+ );
69
+
70
+ /**
71
+ * 表单实例代理:操作全部转发至 currentForm 当前实例;
72
+ * 下游持有本代理即可,:form 运行期替换无需重绑。
73
+ */
74
+ const formProxy = new Proxy({} as F, {
75
+ get(_, prop) {
76
+ const target = currentForm.value as any;
77
+ const val = target?.[prop];
78
+ if (typeof val === 'function') {
79
+ return (...args: any[]) => (currentForm.value as any)?.[prop]?.(...args);
80
+ }
81
+ return val;
82
+ },
83
+ set(_, prop, value) {
84
+ if (currentForm.value) {
85
+ (currentForm.value as any)[prop] = value;
86
+ }
87
+ return true;
88
+ },
89
+ has(_, prop) {
90
+ return Boolean(currentForm.value && prop in currentForm.value);
91
+ },
92
+ });
93
+
94
+ provide(InjectionFormKey, formProxy);
60
95
 
61
- const { formData, fields, setFormRef } = form as F;
62
- const _fields = computed(() => fields.value);
96
+ const fields = computed(() => {
97
+ const f = currentForm.value?.fields;
98
+ return (Array.isArray(f) ? f : f?.value) ?? [];
99
+ });
100
+ const formData = computed(() => currentForm.value?.formData);
101
+ const setFormRef = (el: any) => currentForm.value?.setFormRef?.(el);
102
+
103
+ let formInstanceCounter = 0;
104
+ const formInstanceKeyMap = new WeakMap<object, number>();
105
+ const getFormKey = (f: object) => {
106
+ if (!formInstanceKeyMap.has(f)) {
107
+ formInstanceKeyMap.set(f, ++formInstanceCounter);
108
+ }
109
+ return formInstanceKeyMap.get(f)!;
110
+ };
111
+ const formKey = computed(() => getFormKey(currentForm.value as any));
63
112
 
64
113
  const config = INJECT_CONFIG['pro-form'];
114
+ const injectFormConfig = inject(config.injectionKey, config.default);
65
115
  const attrs = useAttrs();
66
116
 
67
- // 从 ProComponentProvider 获取全局配置,与传入的 props 合并
68
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
69
- const { grid: _injectGrid, ...injectAttrs } = inject(
70
- config.injectionKey,
71
- config.default
72
- );
117
+ /**
118
+ * 从 ProComponentProvider 获取全局配置,响应式派生以支持动态配置变更
119
+ */
120
+ const injectAttrs = computed(() => {
121
+ const raw = toValue(injectFormConfig);
122
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
123
+ const { grid: _injectGrid, ...rest } = raw || {};
124
+ return rest;
125
+ });
73
126
 
74
127
  type ExtractPath<T> = T extends Form<infer D> ? Path<D> : string;
75
128
  type FieldSlotProps = VModelProps &
@@ -82,18 +135,19 @@ const slots = defineSlots<Partial<FieldSlots & { default: Slot }>>();
82
135
  * 动态插槽映射中心 (Slot Teleporting):
83
136
  * 在 setup 同步期直接提供响应式 slots 上下文容器,供深层 BaseField 按需动态获取插槽
84
137
  */
85
- provide(InjectionFormSlotsKey, reactive(slots) as any);
138
+ provide(InjectionFormSlotsKey, slots);
86
139
  </script>
87
140
 
88
141
  <template>
89
142
  <ElForm
90
- :ref="(el: any) => setFormRef?.(el)"
143
+ :key="formKey"
144
+ :ref="setFormRef"
91
145
  :model="formData"
92
146
  v-bind="mergeProps(injectAttrs, camelizeProperties(attrs))"
93
147
  class="pro-form"
94
148
  @submit.prevent
95
149
  >
96
- <BaseFormItem :fields="_fields" :grid="grid" />
150
+ <BaseFormItem :fields="fields" :grid="props.grid" />
97
151
  <slot />
98
152
  </ElForm>
99
153
  </template>
@@ -10,7 +10,9 @@ import { INJECT_CONFIG } from '../../component-provider/constants';
10
10
  import { FORM_ITEM_SLOT_KEYS } from '../constants';
11
11
  import type { Base, Field } from '../types';
12
12
 
13
- const formItemPropKeys = Object.keys(formItemProps());
13
+ const formItemPropKeys = Object.keys(formItemProps()).filter(
14
+ k => k !== 'disabled'
15
+ );
14
16
  const gridItemPropKeys = Object.keys(gridItemProps());
15
17
 
16
18
  export interface GroupedField {
@@ -31,6 +33,7 @@ export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
31
33
  const injectFormItemProps = inject(config.injectionKey, config.default);
32
34
 
33
35
  return computed<GroupedField[]>(() => {
36
+ const rawInjectProps = toValue(injectFormItemProps) || {};
34
37
  return validFields.value.map(field => {
35
38
  let gridItemProps: Record<string, any> = {};
36
39
  let formItemProps: Record<string, any> = {};
@@ -54,8 +57,9 @@ export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
54
57
  lg,
55
58
  xl,
56
59
  ...injectRest
57
- } = injectFormItemProps;
60
+ } = rawInjectProps;
58
61
 
62
+ // 字段显式 span 优先,否则回退注入配置的响应式断点
59
63
  const initGridItemProps =
60
64
  span || span === 0
61
65
  ? { span }
@@ -89,7 +93,7 @@ export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
89
93
  }
90
94
  });
91
95
 
92
- const raw = (toValue(path) ?? toValue(name)) as
96
+ const raw = (toValue(name) ?? toValue(path)) as
93
97
  | string
94
98
  | number[]
95
99
  | undefined;
@@ -105,7 +109,7 @@ export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
105
109
  }
106
110
 
107
111
  const raw =
108
- toValue((field as any)?.path) ?? toValue((field as any)?.name);
112
+ toValue((field as any)?.name) ?? toValue((field as any)?.path);
109
113
  return {
110
114
  path: Array.isArray(raw) ? raw.join('.') : (raw as string) || '',
111
115
  gridItemProps: gridItemProps as GridItemProps,
@@ -30,9 +30,33 @@ export type VModelProps<T = any> = {
30
30
  'onUpdate:modelValue'?: (val: T) => void;
31
31
  };
32
32
 
33
+ /**
34
+ * 字段值格式化/转换处理器
35
+ *
36
+ * @description 支持函数形态或包含 get/set 方法的对象形态。
37
+ *
38
+ * ### 行为与契约说明
39
+ * 1. **旧值参数按需注入**:底层基于函数的形参声明个数(`fn.length > 1`)决定是否读取旧值。若转换逻辑需要使用旧值,请显式声明两个形参 `(val, oldVal) => ...`(请勿使用默认参数或 `...args` 剩余参数,否则 `fn.length` 无法正确识别);
40
+ * 2. **只读快照副本**:传入的 `oldVal` 为只读快照副本,不得依赖原地修改写回;仅当 formatter 声明双形参时才克隆旧值,单参转换零开销。
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // 1. 单参数转换(常见):无需旧值,零多余开销
45
+ * valueFormatter: (val) => val?.trim()
46
+ *
47
+ * // 2. 双参数转换:需要基于旧值做对比或回退
48
+ * valueFormatter: (val, oldVal) => (isValid(val) ? val : oldVal)
49
+ *
50
+ * // 3. 对象形态:分别定义读(展示)与写(存储)转换
51
+ * valueFormatter: {
52
+ * get: (val) => (val ? dayjs(val).format('YYYY-MM-DD') : ''),
53
+ * set: (val, oldVal) => (val ? dayjs(val).valueOf() : undefined),
54
+ * }
55
+ * ```
56
+ */
33
57
  export type ValueFormatter =
34
- | { (val: any, oldVal: any): any }
35
- | { get?: (val: any) => any; set?: (val: any, oldVal: any) => any };
58
+ | { (val: any, oldVal?: any): any }
59
+ | { get?: (val: any) => any; set?: (val: any, oldVal?: any) => any };
36
60
 
37
61
  /**
38
62
  * @description 自定义组件