@qin-ui/element-plus-pro 1.2.2 → 1.2.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 (38) hide show
  1. package/api.json +1 -1
  2. package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -1
  3. package/es/components/form/components/ProForm/index.vue.d.ts.map +1 -1
  4. package/es/components/form/constants/index.d.ts +1 -6
  5. package/es/components/form/constants/index.d.ts.map +1 -1
  6. package/es/components/form/hooks/useGroupFieldAttrs.d.ts +18 -0
  7. package/es/components/form/hooks/useGroupFieldAttrs.d.ts.map +1 -0
  8. package/es/components/table/components/ColumnControl.vue.d.ts.map +1 -1
  9. package/es/components/table/components/ProTable.vue.d.ts.map +1 -1
  10. package/es/components/table/components/SearchForm.vue.d.ts.map +1 -1
  11. package/es/components/table/components/TableColumnRenderer.vue.d.ts.map +1 -1
  12. package/es/components/table/types/index.d.ts +2 -0
  13. package/es/components/table/types/index.d.ts.map +1 -1
  14. package/es/components/table/useTable.d.ts.map +1 -1
  15. package/es/element-plus-pro.css +12 -12
  16. package/es/index.js +201 -177
  17. package/es/index.js.map +1 -1
  18. package/es/shared/core/index.d.ts +1 -1
  19. package/es/shared/core/index.d.ts.map +1 -1
  20. package/package.json +2 -2
  21. package/src/components/form/components/BaseField/index.vue +8 -8
  22. package/src/components/form/components/BaseFormItem/index.vue +47 -66
  23. package/src/components/form/components/ProForm/index.vue +4 -11
  24. package/src/components/form/constants/index.ts +1 -7
  25. package/src/components/form/hooks/useGroupFieldAttrs.ts +119 -0
  26. package/src/components/table/components/ColumnControl.vue +2 -1
  27. package/src/components/table/components/ProTable.vue +3 -23
  28. package/src/components/table/components/SearchForm.vue +81 -15
  29. package/src/components/table/components/TableColumnRenderer.vue +28 -14
  30. package/src/components/table/types/index.ts +2 -0
  31. package/src/components/table/useTable.ts +1 -10
  32. package/src/shared/core/index.ts +1 -1
  33. package/es/components/form/components/GroupedFieldAttrs/index.vue.d.ts +0 -31
  34. package/es/components/form/components/GroupedFieldAttrs/index.vue.d.ts.map +0 -1
  35. package/es/components/form/components/PathProvider/index.vue.d.ts +0 -24
  36. package/es/components/form/components/PathProvider/index.vue.d.ts.map +0 -1
  37. package/src/components/form/components/GroupedFieldAttrs/index.vue +0 -115
  38. package/src/components/form/components/PathProvider/index.vue +0 -19
@@ -2,5 +2,5 @@
2
2
  * 统一 re-export @qin-ui/pro-components-core,包内所有文件通过此文件引入 core 内容,
3
3
  * 避免直接散列依赖 @qin-ui/pro-components-core,便于将来统一维护。
4
4
  */
5
- export { useForm, useTable, useFields, useFormData, useFormRef, InjectionFormKey, InjectionPathKey, get, getObject, toPath, camelizeProperties, cloneDeep, omit, pick, isPlainObject, 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
+ export { useForm, useTable, useFields, useFormData, useFormRef, InjectionFormKey, InjectionFormSlotsKey, get, getObject, toPath, camelizeProperties, cloneDeep, omit, pick, isPlainObject, 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';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/core/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAEL,OAAO,EACP,QAAQ,EACR,SAAS,EACT,WAAW,EACX,UAAU,EAEV,gBAAgB,EAChB,gBAAgB,EAEhB,GAAG,EACH,SAAS,EACT,MAAM,EACN,kBAAkB,EAClB,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,aAAa,EAEb,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;;;GAGG;AACH,OAAO,EAEL,OAAO,EACP,QAAQ,EACR,SAAS,EACT,WAAW,EACX,UAAU,EAEV,gBAAgB,EAChB,qBAAqB,EAErB,GAAG,EACH,SAAS,EACT,MAAM,EACN,kBAAkB,EAClB,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,aAAa,EAEb,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.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "基于 element-plus 的二次封装组件",
5
5
  "bin": {
6
6
  "element-plus-pro": "./bin/init-ai.mjs"
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "vue-component-type-helpers": "^3.2.5",
42
- "@qin-ui/pro-components-core": "^1.1.0"
42
+ "@qin-ui/pro-components-core": "^1.1.3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "element-plus": "^2.13.6"
@@ -39,7 +39,7 @@ import { cloneDeep, omit } from '../../../../shared/core';
39
39
  import { useDisabledContext } from '../../../../shared/ui';
40
40
  import type { Field } from '../../types';
41
41
  import { ContainerFragment, SlotComponent } from '..';
42
- import { componentMap, TeleportComponentNamePrefix } from '../../constants';
42
+ import { componentMap, InjectionFormSlotsKey } from '../../constants';
43
43
  import { INJECT_COMPONENTS } from '../../../component-provider/constants';
44
44
  import { useForm } from '../../hooks/useForm';
45
45
  import { getInitProps } from './utils';
@@ -134,21 +134,21 @@ const groupedAttrs = computed(() => {
134
134
  };
135
135
  });
136
136
 
137
- // 从 ProForm 插槽注入的 teleport 组件(优先级最高)
138
- const teleportComponent = inject(
139
- `${TeleportComponentNamePrefix}${path}`,
140
- undefined
141
- );
137
+ // 从 ProForm 插槽注入的 slots 集合(优先级最高)
138
+ const formSlots = inject(InjectionFormSlotsKey, undefined) as
139
+ | Record<string, any>
140
+ | undefined;
142
141
 
143
142
  // 从 ProComponentProvider 注入的自定义组件映射
144
143
  const customComponents = inject(INJECT_COMPONENTS, {});
145
144
 
146
145
  /*
147
146
  * 按优先级解析最终要渲染的组件:
148
- * teleport 插槽 → 自定义组件映射 → 内置组件映射 → 原始 component 值
147
+ * ProForm 插槽 → 自定义组件映射 → 内置组件映射 → 原始 component 值
149
148
  */
150
149
  const is = computed(() => {
151
- if (teleportComponent) return teleportComponent;
150
+ const teleportSlot = path && formSlots?.[path];
151
+ if (teleportSlot) return teleportSlot;
152
152
  if (typeof component === 'string') {
153
153
  return (
154
154
  (customComponents as any)[component] ||
@@ -13,12 +13,9 @@
13
13
  * ## 渲染流程
14
14
  *
15
15
  * 1. 遍历 fields 数组,过滤 hidden 字段
16
- * 2. 对每个字段:
17
- * a. 通过 PathProvider 提供当前字段路径
18
- * b. 通过 GroupedFieldAttrs 将 field 配置拆分为 gridItemProps / formItemProps / componentProps
19
- * c. 如果字段有 `fields` 子字段 → 递归渲染 BaseFormItem(嵌套表单)
20
- * d. 否则 → 渲染 BaseField(输入组件)
21
- * 3. 通过 ref 回调收集 FormItem 和 Component 实例,注入到 field 对象上
16
+ * 2. 通过 useGroupFieldAttrs Hook 纯计算将 field 配置拆分为 gridItemProps / formItemProps / componentProps / slots
17
+ * 3. 递归渲染 BaseFormItem(嵌套表单)或 BaseField(输入组件)
18
+ * 4. 通过 ref 回调收集 FormItem 和 Component 实例,注入到 field 对象上
22
19
  *
23
20
  * @param {Fields<D>} [fields] - 字段配置数组
24
21
  * @param {boolean | GridProps} [grid] - 是否启用网格布局
@@ -37,8 +34,7 @@ import {
37
34
  import { computed, inject, toValue } from 'vue';
38
35
  import type { Fields, Grid } from '../../types';
39
36
  import { BaseField, SlotComponent, ContainerFragment } from '..';
40
- import PathProvider from '../PathProvider/index.vue';
41
- import GroupedFieldAttrs from '../GroupedFieldAttrs/index.vue';
37
+ import { useGroupFieldAttrs } from '../../hooks/useGroupFieldAttrs';
42
38
  import { INJECT_CONFIG } from '../../../component-provider/constants';
43
39
 
44
40
  defineOptions({ name: 'BaseFormItem', inheritAttrs: false });
@@ -92,6 +88,8 @@ const onComponentMounted = (index: number) => {
92
88
  componentRefs[index]?.getComponentComputedProps(),
93
89
  });
94
90
  };
91
+
92
+ const groupedFields = useGroupFieldAttrs(validFields);
95
93
  </script>
96
94
 
97
95
  <template>
@@ -100,66 +98,49 @@ const onComponentMounted = (index: number) => {
100
98
  v-bind="computedGridProps"
101
99
  style="width: 100%"
102
100
  >
103
- <PathProvider
104
- v-for="({ path: _path, ...field }, index) of validFields"
105
- :key="toPath(toValue((_path ?? index) as any)).join('.') || index"
106
- v-bind="{ path: _path as any }"
107
- >
108
- <template #default="{ path }">
109
- <GroupedFieldAttrs :field="field">
110
- <template
111
- #default="{
112
- gridItemProps,
113
- formItemProps: { container, ...formItemProps },
114
- componentProps,
115
- formItemSlots,
116
- }"
101
+ <template v-for="(item, index) of groupedFields" :key="item.path || index">
102
+ <ContainerFragment
103
+ :component="enableGrid ? ElGridItem : undefined"
104
+ v-bind="item.gridItemProps"
105
+ >
106
+ <ContainerFragment :component="item.container" :path="item.path">
107
+ <FormItem
108
+ v-bind="item.formItemProps"
109
+ :ref="el => (formItemRefs[index] = el)"
110
+ :prop="toPath(item.path).join('.')"
111
+ :path="item.path"
112
+ @vue:mounted="onFormItemMounted(index, item.formItemProps)"
117
113
  >
118
- <ContainerFragment
119
- :component="enableGrid ? ElGridItem : undefined"
120
- v-bind="gridItemProps"
114
+ <template
115
+ v-for="(slot, name) in item.formItemSlots"
116
+ :key="name"
117
+ #[name]="scoped"
121
118
  >
122
- <ContainerFragment :component="container" :path="path">
123
- <FormItem
124
- v-bind="formItemProps"
125
- :ref="el => (formItemRefs[index] = el)"
126
- :prop="toPath(path).join('.')"
127
- :path="path"
128
- @vue:mounted="onFormItemMounted(index, formItemProps)"
129
- >
130
- <template
131
- v-for="(slot, name) in formItemSlots"
132
- :key="name"
133
- #[name]="scoped"
134
- >
135
- <SlotComponent
136
- :path="path"
137
- :component="slot"
138
- v-bind="scoped"
139
- />
140
- </template>
141
- <template v-if="field.fields">
142
- <BaseFormItem
143
- :grid="(field as any).grid ?? grid"
144
- :fields="field.fields"
145
- :disabled="toValue(field.disabled)"
146
- />
147
- </template>
148
- <template v-else>
149
- <BaseField
150
- :ref="el => (componentRefs[index] = el)"
151
- v-bind="componentProps"
152
- :path="path"
153
- @vue:mounted="onComponentMounted(index)"
154
- />
155
- </template>
156
- </FormItem>
157
- </ContainerFragment>
158
- </ContainerFragment>
159
- </template>
160
- </GroupedFieldAttrs>
161
- </template>
162
- </PathProvider>
119
+ <SlotComponent
120
+ :path="item.path"
121
+ :component="slot"
122
+ v-bind="scoped"
123
+ />
124
+ </template>
125
+ <template v-if="item.rawField.fields">
126
+ <BaseFormItem
127
+ :grid="(item.rawField as any).grid ?? grid"
128
+ :fields="item.rawField.fields"
129
+ :disabled="toValue(item.rawField.disabled)"
130
+ />
131
+ </template>
132
+ <template v-else>
133
+ <BaseField
134
+ :ref="el => (componentRefs[index] = el)"
135
+ v-bind="item.componentProps"
136
+ :path="item.path"
137
+ @vue:mounted="onComponentMounted(index)"
138
+ />
139
+ </template>
140
+ </FormItem>
141
+ </ContainerFragment>
142
+ </ContainerFragment>
143
+ </template>
163
144
  </ContainerFragment>
164
145
  </template>
165
146
 
@@ -32,14 +32,14 @@ import {
32
32
  inject,
33
33
  mergeProps,
34
34
  provide,
35
+ reactive,
35
36
  type Slot,
36
- watchEffect,
37
37
  computed,
38
38
  useAttrs,
39
39
  } from 'vue';
40
40
  import { INJECT_CONFIG } from '../../../component-provider/constants';
41
41
  import { BaseFormItem } from '..';
42
- import { InjectionFormKey, TeleportComponentNamePrefix } from '../../constants';
42
+ import { InjectionFormKey, InjectionFormSlotsKey } from '../../constants';
43
43
  import type { Grid, VModelProps, PathProps } from '../../types';
44
44
  import { Path, camelizeProperties } from '../../../../shared/core';
45
45
  import type { Form } from '../../hooks/useForm';
@@ -80,16 +80,9 @@ const slots = defineSlots<Partial<FieldSlots & { default: Slot }>>();
80
80
 
81
81
  /**
82
82
  * 动态插槽映射中心 (Slot Teleporting):
83
- * 目的:将 ProForm 上以 Field Path 命名的插槽,动态注册为全局可访问的 Context。
84
- * 实现:BaseField 组件在渲染对应字段时,会通过 inject 尝试获取对应的 Prefix+Path,
85
- * 如果存在,则将插槽内容渲染到深层的具体位置,从而突破了组件嵌套层级的限制。
83
+ * 在 setup 同步期直接提供响应式 slots 上下文容器,供深层 BaseField 按需动态获取插槽
86
84
  */
87
- watchEffect(() => {
88
- Object.keys(slots).forEach(name => {
89
- if (name === 'default') return;
90
- provide(`${TeleportComponentNamePrefix}${name}`, (slots as any)[name]);
91
- });
92
- });
85
+ provide(InjectionFormSlotsKey, reactive(slots) as any);
93
86
  </script>
94
87
 
95
88
  <template>
@@ -106,11 +106,5 @@ export const componentMap: BaseComponentMap = {
106
106
  'transfer': Transfer,
107
107
  }
108
108
 
109
- /**
110
- * Teleport 组件名称前缀
111
- * @description 用于通过插槽动态替换指定 path 的组件
112
- */
113
- export const TeleportComponentNamePrefix = 'TeleportComponent_';
114
-
115
109
  // inject keys
116
- export { InjectionFormKey, InjectionPathKey } from '../../../shared/core';
110
+ export { InjectionFormKey, InjectionFormSlotsKey } from '../../../shared/core';
@@ -0,0 +1,119 @@
1
+ import { computed, inject, mergeProps, toValue, type ComputedRef } from 'vue';
2
+ import {
3
+ formItemProps,
4
+ gridItemProps,
5
+ type FormItemProps,
6
+ type GridItemProps,
7
+ } from '../../../shared/ui';
8
+ import { isPlainObject } from '../../../shared/core';
9
+ import { INJECT_CONFIG } from '../../component-provider/constants';
10
+ import { FORM_ITEM_SLOT_KEYS } from '../constants';
11
+ import type { Base, Field } from '../types';
12
+
13
+ const formItemPropKeys = Object.keys(formItemProps());
14
+ const gridItemPropKeys = Object.keys(gridItemProps());
15
+
16
+ export interface GroupedField {
17
+ path: string;
18
+ gridItemProps: GridItemProps;
19
+ formItemProps: FormItemProps;
20
+ formItemSlots: Record<(typeof FORM_ITEM_SLOT_KEYS)[number], any>;
21
+ componentProps: Record<string, any>;
22
+ container?: Base['formItemContainer'];
23
+ rawField: Field;
24
+ }
25
+
26
+ /**
27
+ * 将 fields 配置数组逐个拆分为 grid / formItem / component / slots 分组。
28
+ */
29
+ export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
30
+ const config = INJECT_CONFIG['pro-form-item'];
31
+ const injectFormItemProps = inject(config.injectionKey, config.default);
32
+
33
+ return computed<GroupedField[]>(() => {
34
+ return validFields.value.map(field => {
35
+ let gridItemProps: Record<string, any> = {};
36
+ let formItemProps: Record<string, any> = {};
37
+ const componentProps: Record<string, any> = { slots: {} };
38
+ const formItemSlots: Record<string, any> = {};
39
+
40
+ if (isPlainObject(field)) {
41
+ // prettier-ignore
42
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
43
+ const { path, name, fields, formItemClass, formItemStyle, formItemContainer, hidden, span, getFormItemRef, getComponentRef, getFormItemComputedProps, getComponentComputedProps, slots = {}, formItemDataAttrs = {}, componentDataAttrs = {}, extraProps, ...rest } =
44
+ field as any;
45
+
46
+ const {
47
+ class: injectClassName,
48
+ style: injectStyle,
49
+ span: injectSpan,
50
+ formItemContainer: injectFormItemContainer,
51
+ xs,
52
+ sm,
53
+ md,
54
+ lg,
55
+ xl,
56
+ ...injectRest
57
+ } = injectFormItemProps;
58
+
59
+ const initGridItemProps =
60
+ span || span === 0
61
+ ? { span }
62
+ : { xs, sm, md, lg, xl, span: injectSpan };
63
+
64
+ gridItemProps = fields ? { span: span ?? 24 } : initGridItemProps;
65
+ formItemProps = mergeProps(
66
+ injectRest as any,
67
+ { class: injectClassName, style: injectStyle },
68
+ { class: toValue(formItemClass), style: toValue(formItemStyle) },
69
+ formItemDataAttrs
70
+ );
71
+ const container = formItemContainer ?? injectFormItemContainer;
72
+
73
+ Object.keys(rest).forEach(k => {
74
+ if (gridItemPropKeys.includes(k)) {
75
+ gridItemProps[k] = rest[k];
76
+ } else if (formItemPropKeys.includes(k)) {
77
+ (formItemProps as any)[k] = rest[k];
78
+ } else {
79
+ componentProps[k] = rest[k];
80
+ }
81
+ });
82
+
83
+ Object.assign(componentProps, componentDataAttrs);
84
+ Object.keys(slots).forEach(k => {
85
+ if (FORM_ITEM_SLOT_KEYS.includes(k as any)) {
86
+ formItemSlots[k] = slots[k];
87
+ } else {
88
+ componentProps.slots[k] = slots[k];
89
+ }
90
+ });
91
+
92
+ const raw = (toValue(path) ?? toValue(name)) as
93
+ | string
94
+ | number[]
95
+ | undefined;
96
+ return {
97
+ path: Array.isArray(raw) ? raw.join('.') : (raw as string) || '',
98
+ gridItemProps: gridItemProps as GridItemProps,
99
+ formItemProps: formItemProps as any,
100
+ formItemSlots,
101
+ componentProps,
102
+ container,
103
+ rawField: field,
104
+ };
105
+ }
106
+
107
+ const raw =
108
+ toValue((field as any)?.path) ?? toValue((field as any)?.name);
109
+ return {
110
+ path: Array.isArray(raw) ? raw.join('.') : (raw as string) || '',
111
+ gridItemProps: gridItemProps as GridItemProps,
112
+ formItemProps: formItemProps as any,
113
+ formItemSlots,
114
+ componentProps,
115
+ rawField: field,
116
+ };
117
+ });
118
+ });
119
+ }
@@ -31,7 +31,7 @@ const indeterminate = ref(false);
31
31
 
32
32
  const getColumnKey = (column: Column, index: number) => {
33
33
  const key = column.key ?? column.prop;
34
- if (key === undefined || key === null) return `__idx_${index}`;
34
+ if (key === undefined || key === null) return `__col_${index}`;
35
35
  return Array.isArray(key) ? key.join('.') : String(key);
36
36
  };
37
37
 
@@ -55,6 +55,7 @@ const checkedColumnsOptions = computed<
55
55
  );
56
56
  const label = String(
57
57
  item.label ??
58
+ (item as any).title ??
58
59
  item.prop ??
59
60
  t('table.columnFallback', { index: index + 1 })
60
61
  );
@@ -220,22 +220,6 @@ const computedTableContainer = computed(() => {
220
220
 
221
221
  const normalizedRawColumns = computed(() => {
222
222
  const source = (columns ?? tableColumns?.value ?? []) as any[];
223
-
224
- const normalized = source.map((item, index) => {
225
- const current: any = { ...item };
226
- if (current.label === undefined && current.title !== undefined) {
227
- current.label = current.title;
228
- }
229
- if (current.key === undefined) {
230
- current.key =
231
- current.prop ??
232
- (typeof current.columnKey === 'string'
233
- ? current.columnKey
234
- : undefined) ??
235
- `col-${index}`;
236
- }
237
- return current;
238
- });
239
223
  if (addIndexColumn ?? injectProps.addIndexColumn) {
240
224
  return [
241
225
  {
@@ -244,10 +228,10 @@ const normalizedRawColumns = computed(() => {
244
228
  label: t('table.index'),
245
229
  width: 80,
246
230
  },
247
- ...normalized,
231
+ ...source,
248
232
  ];
249
233
  }
250
- return normalized;
234
+ return source;
251
235
  });
252
236
 
253
237
  const computedDataSource = computed<any[]>(() => {
@@ -336,11 +320,7 @@ onMounted(() => {
336
320
  </script>
337
321
 
338
322
  <template>
339
- <div
340
- class="pro-table"
341
- :class="($attrs as any).class"
342
- :style="($attrs as any).style"
343
- >
323
+ <div class="pro-table" :class="attrs.class" :style="attrs.style">
344
324
  <ContainerFragment
345
325
  v-if="showSearch"
346
326
  :component="computedSearchFormConfig.container"
@@ -61,8 +61,10 @@ import {
61
61
  AllowedComponentProps,
62
62
  computed,
63
63
  ref,
64
+ nextTick,
64
65
  watch,
65
66
  watchEffect,
67
+ onBeforeUnmount,
66
68
  type Component,
67
69
  type DefineComponent,
68
70
  } from 'vue';
@@ -94,7 +96,25 @@ const emit = defineEmits<Emits>();
94
96
 
95
97
  const proFormHeight = ref<'unset' | number>('unset');
96
98
  const collapseHeight = ref(0);
97
- let rowHeight = 32;
99
+ const rowHeight = ref(32);
100
+
101
+ let resizeObserver: ResizeObserver | null = null;
102
+ let intersectionObserver: IntersectionObserver | null = null;
103
+
104
+ const cleanupObservers = () => {
105
+ if (resizeObserver) {
106
+ resizeObserver.disconnect();
107
+ resizeObserver = null;
108
+ }
109
+ if (intersectionObserver) {
110
+ intersectionObserver.disconnect();
111
+ intersectionObserver = null;
112
+ }
113
+ };
114
+
115
+ onBeforeUnmount(() => {
116
+ cleanupObservers();
117
+ });
98
118
 
99
119
  const computedExpand = computed(() => {
100
120
  if (!expand) return false;
@@ -109,11 +129,41 @@ const showExpandToggle = ref(false);
109
129
 
110
130
  const expandStatus = ref(true);
111
131
 
132
+ const { formRef, getFormData } = form;
133
+
134
+ const updateHeights = () => {
135
+ if (!expand || !formRef.value) return;
136
+ const proFormEl = (formRef.value as any)?.$el;
137
+ if (!proFormEl) return;
138
+
139
+ const itemHeight = proFormEl
140
+ ?.querySelector("[class*='-form-item']")
141
+ ?.getBoundingClientRect?.()?.height;
142
+ if (itemHeight && itemHeight > 0) {
143
+ rowHeight.value = itemHeight;
144
+ }
145
+
146
+ // 使用 scrollHeight 测量自然展开高度,突破固定高度与 overflow: hidden 样式的自引用限制
147
+ const naturalHeight =
148
+ proFormEl.scrollHeight || proFormEl.getBoundingClientRect?.()?.height || 0;
149
+ if (naturalHeight > 0) {
150
+ proFormHeight.value = naturalHeight;
151
+ }
152
+ };
153
+
112
154
  const changeExpandStatus = () => {
113
155
  expandStatus.value = !expandStatus.value;
156
+ nextTick(() => {
157
+ updateHeights();
158
+ });
114
159
  };
115
160
 
116
- const { formRef, getFormData } = form;
161
+ const hasFieldValue = (val: any) =>
162
+ val !== null &&
163
+ val !== undefined &&
164
+ val !== '' &&
165
+ !(Array.isArray(val) && val.length === 0) &&
166
+ !(typeof val === 'object' && Object.keys(val).length === 0);
117
167
 
118
168
  /*
119
169
  * 使用 IntersectionObserver 检测折叠状态下隐藏字段是否有值
@@ -123,26 +173,30 @@ const setInitExpandStatus = () => {
123
173
  expandStatus.value = false;
124
174
  if (formRef.value && expand) {
125
175
  const formEl = (formRef.value as any).$el;
126
- const formItemsEl = formEl.querySelectorAll(
176
+ const formItemsEl = formEl?.querySelectorAll?.(
127
177
  "[class*='-form-item'] > [path]"
128
178
  );
129
- const observer = new IntersectionObserver(
179
+ if (!formItemsEl || formItemsEl.length === 0) return;
180
+ if (intersectionObserver) {
181
+ intersectionObserver.disconnect();
182
+ }
183
+ intersectionObserver = new IntersectionObserver(
130
184
  entries => {
131
185
  expandStatus.value = entries.some(e => {
132
186
  // 如果隐藏字段(intersectionRatio === 0)有值,则自动展开
133
187
  if (e.intersectionRatio === 0) {
134
188
  const path = e.target.getAttribute('path');
135
189
  const searchFieldValue = path ? getFormData?.(path) : undefined;
136
- return ![null, undefined].includes(searchFieldValue);
190
+ return hasFieldValue(searchFieldValue);
137
191
  }
138
192
  return (expand as any).expandStatus;
139
193
  });
140
- observer.disconnect();
194
+ intersectionObserver?.disconnect();
141
195
  },
142
196
  { root: formEl }
143
197
  );
144
198
  formItemsEl.forEach((element: Element) => {
145
- observer.observe(element);
199
+ intersectionObserver?.observe(element);
146
200
  });
147
201
  }
148
202
  };
@@ -154,17 +208,29 @@ watch(
154
208
  () => expand,
155
209
  ],
156
210
  () => {
157
- if (!expand || !expandStatus.value || !formRef.value) return;
158
- const proFormEl = (formRef.value as any)?.$el;
159
- const { height = 0 } = proFormEl?.getBoundingClientRect?.() || {};
160
- proFormHeight.value = height;
161
- rowHeight = proFormEl
162
- ?.querySelector("[class*='-form-item']")
163
- ?.getBoundingClientRect?.()?.height;
211
+ updateHeights();
164
212
  },
165
213
  { flush: 'post', immediate: true }
166
214
  );
167
215
 
216
+ watch(
217
+ () => formRef.value,
218
+ val => {
219
+ if (resizeObserver) {
220
+ resizeObserver.disconnect();
221
+ resizeObserver = null;
222
+ }
223
+ const formEl = (val as any)?.$el;
224
+ if (formEl && typeof ResizeObserver !== 'undefined') {
225
+ resizeObserver = new ResizeObserver(() => {
226
+ updateHeights();
227
+ });
228
+ resizeObserver.observe(formEl);
229
+ }
230
+ },
231
+ { immediate: true }
232
+ );
233
+
168
234
  /*
169
235
  * 计算折叠高度和是否显示展开按钮
170
236
  * 折叠高度 = minExpandRows * 行高 + (minExpandRows - 1) * 行间距
@@ -176,7 +242,7 @@ watchEffect(
176
242
  if (layout === 'grid' && computedExpand.value) {
177
243
  const { minExpandRows } = computedExpand.value;
178
244
  collapseHeight.value = Math.min(
179
- minExpandRows * rowHeight + (minExpandRows - 1) * rowGap,
245
+ minExpandRows * rowHeight.value + (minExpandRows - 1) * rowGap,
180
246
  +proFormHeight.value
181
247
  );
182
248
  showExpandToggle.value = +proFormHeight.value - collapseHeight.value > 1;