@qin-ui/antd-vue-pro 2.5.2 → 2.6.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 (48) hide show
  1. package/AI-CONTEXT.md +30 -10
  2. package/api.json +24 -4
  3. package/es/antd-vue-pro.css +6 -6
  4. package/es/components/component-provider/types/index.d.ts +1 -1
  5. package/es/components/component-provider/types/index.d.ts.map +1 -1
  6. package/es/components/form/components/BaseField/index.vue.d.ts.map +1 -1
  7. package/es/components/form/components/BaseFormItem/index.vue.d.ts +59 -11
  8. package/es/components/form/components/BaseFormItem/index.vue.d.ts.map +1 -1
  9. package/es/components/form/components/BaseFormItems/index.vue.d.ts +16 -0
  10. package/es/components/form/components/BaseFormItems/index.vue.d.ts.map +1 -0
  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 +13 -3
  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 +3 -1
  18. package/es/components/table/components/ProTable.vue.d.ts.map +1 -1
  19. package/es/components/table/components/TableEllipsisTooltip.vue.d.ts +19 -0
  20. package/es/components/table/components/TableEllipsisTooltip.vue.d.ts.map +1 -0
  21. package/es/components/table/hooks/useEllipsisTooltip.d.ts +45 -0
  22. package/es/components/table/hooks/useEllipsisTooltip.d.ts.map +1 -0
  23. package/es/components/table/hooks/useEllipsisTooltipColumns.d.ts +19 -0
  24. package/es/components/table/hooks/useEllipsisTooltipColumns.d.ts.map +1 -0
  25. package/es/components/table/index.d.ts +1 -1
  26. package/es/components/table/index.d.ts.map +1 -1
  27. package/es/components/table/types/index.d.ts +46 -2
  28. package/es/components/table/types/index.d.ts.map +1 -1
  29. package/es/index.js +718 -228
  30. package/es/index.js.map +1 -1
  31. package/es/shared/ui/index.d.ts +2 -1
  32. package/es/shared/ui/index.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/components/component-provider/types/index.ts +1 -0
  35. package/src/components/form/components/BaseField/index.vue +30 -9
  36. package/src/components/form/components/BaseFormItem/index.vue +153 -186
  37. package/src/components/form/components/BaseFormItems/index.vue +186 -0
  38. package/src/components/form/components/ProForm/index.vue +5 -5
  39. package/src/components/form/components/index.ts +6 -6
  40. package/src/components/form/hooks/useGroupFieldAttrs.ts +114 -88
  41. package/src/components/form/utils/fieldKey.ts +63 -0
  42. package/src/components/table/components/ProTable.vue +128 -56
  43. package/src/components/table/components/TableEllipsisTooltip.vue +90 -0
  44. package/src/components/table/hooks/useEllipsisTooltip.ts +244 -0
  45. package/src/components/table/hooks/useEllipsisTooltipColumns.ts +139 -0
  46. package/src/components/table/index.ts +7 -1
  47. package/src/components/table/types/index.ts +62 -2
  48. package/src/shared/ui/index.ts +2 -0
@@ -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. 递归渲染:网格布局和具体字段的解析交由 `<BaseFormItem>` 递归完成。
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 BaseFormItem from '../BaseFormItem/index.vue';
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';
@@ -82,7 +82,7 @@ const props = withDefaults(defineProps<Props>(), {
82
82
  /**
83
83
  * 依赖注入中心:
84
84
  * 未传 form 实例(含运行期置空)时回退内部表单实例;
85
- * 将 Form 实例注入到 Vue Context 中,供深层子组件(如 BaseFormItem, BaseField, 自定义插槽)跨层级直接访问数据和方法。
85
+ * 将 Form 实例注入到 Vue Context 中,供深层子组件(如 BaseFormItems, BaseField, 自定义插槽)跨层级直接访问数据和方法。
86
86
  */
87
87
  // 兜底表单实例:setup 同步期创建,未传 :form(含运行期置空)时经 currentForm 生效
88
88
  const fallbackForm = useForm() as unknown as F;
@@ -165,7 +165,7 @@ provide(InjectionFormSlotsKey, formSlotsRef);
165
165
  v-bind="formAttrs"
166
166
  class="pro-form"
167
167
  >
168
- <BaseFormItem :fields="fields" :grid="props.grid" />
168
+ <BaseFormItems :fields="fields" :grid="props.grid" />
169
169
  <slot />
170
170
  </AForm>
171
171
  </template>
@@ -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 BaseFormItem from './BaseFormItem/index.vue';
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, BaseFormItem };
11
+ export { ProForm, BaseFormItems };
12
12
 
13
13
  // instance
14
14
  /** ProForm 的组件实例类型,用于 `ref` 标注与 `defineExpose` 访问 */
@@ -16,7 +16,7 @@ export type ProFormInstance = ComponentExposed<typeof ProForm>;
16
16
  /** ProForm 的组件属性类型 */
17
17
  export type ProFormProps = ComponentProps<typeof ProForm>;
18
18
 
19
- /** BaseFormItem 的组件实例类型 */
20
- export type ProFormItemInstance = ComponentExposed<typeof BaseFormItem>;
21
- /** BaseFormItem 的组件属性类型 */
22
- export type ProFormItemProps = ComponentProps<typeof BaseFormItem>;
19
+ /** BaseFormItems 的组件实例类型 */
20
+ export type ProFormItemsInstance = ComponentExposed<typeof BaseFormItems>;
21
+ /** BaseFormItems 的组件属性类型 */
22
+ export type ProFormItemsProps = ComponentProps<typeof BaseFormItems>;
@@ -1,5 +1,11 @@
1
- import { computed, inject, mergeProps, toValue, type ComputedRef } from 'vue';
2
- import { getFieldKey } from '../utils/fieldKey';
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,
@@ -35,16 +41,102 @@ export interface GroupedField {
35
41
  formItemContainer?: Base['formItemContainer'];
36
42
  /** 字段原始对象,供实例登记与访问器挂载使用 */
37
43
  rawField: Field;
38
- /** 渲染 key,取自字段对象身份 */
39
- renderKey: string | number;
40
44
  }
41
45
 
46
+ /** 将单个字段配置拆分为 grid / formItem / component / slots 分组 */
47
+ const groupField = (
48
+ field: Field,
49
+ rawInject: Record<string, any>
50
+ ): GroupedField => {
51
+ let gridItemProps: Record<string, any> = {};
52
+ let formItemProps: Record<string, any> = {};
53
+ const componentProps: Record<string, any> = { slots: {} };
54
+ const formItemSlots: Record<string, any> = {};
55
+
56
+ if (isPlainObject(field)) {
57
+ // prettier-ignore
58
+ const { path: _path, name: _name, fields, formItemClass, formItemStyle, hidden, span, getFormItemRef, getComponentRef, getFormItemComputedProps, getComponentComputedProps, slots = {}, formItemDataAttrs = {}, componentDataAttrs = {}, extraProps, formItemContainer, ...rest } =
59
+ field as any;
60
+
61
+ // prettier-ignore
62
+ const { class: injectClassName, style: injectStyle, span: injectSpan, xs, sm, md, lg, xl, xxl, formItemContainer: injectFormItemContainer, ...injectRest } = rawInject;
63
+
64
+ /*
65
+ * 字段显式指定 span 时整体接管栅格,不再叠加注入的响应式断点;
66
+ * 未指定 span 时使用注入的断点配置
67
+ */
68
+ const initGridItemProps =
69
+ span || span === 0
70
+ ? { span }
71
+ : { xs, sm, md, lg, xl, xxl, span: injectSpan };
72
+
73
+ // 分组字段未指定 span 时整行渲染(span 兜底 24),注入的响应式断点保持生效
74
+ gridItemProps = fields
75
+ ? { ...initGridItemProps, span: span ?? 24 }
76
+ : initGridItemProps;
77
+ // 字段级容器优先,回退注入级配置
78
+ const container = formItemContainer ?? injectFormItemContainer;
79
+ formItemProps = mergeProps(
80
+ injectRest,
81
+ { class: injectClassName, style: injectStyle },
82
+ { class: toValue(formItemClass), style: toValue(formItemStyle) },
83
+ formItemDataAttrs
84
+ );
85
+
86
+ Object.keys(rest).forEach(k => {
87
+ if (gridItemPropKeySet.has(k)) {
88
+ gridItemProps[k] = rest[k];
89
+ } else if (formItemPropKeySet.has(k)) {
90
+ formItemProps[k] = rest[k];
91
+ } else {
92
+ componentProps[k] = rest[k];
93
+ }
94
+ });
95
+
96
+ Object.assign(componentProps, componentDataAttrs);
97
+ Object.keys(slots).forEach(k => {
98
+ if (formItemSlotKeySet.has(k)) {
99
+ formItemSlots[k] = slots[k];
100
+ } else {
101
+ componentProps.slots[k] = slots[k];
102
+ }
103
+ });
104
+
105
+ // label 插槽与字段 label 同时存在时由插槽渲染,剥离 label 属性
106
+ if (formItemSlots.label && formItemProps.label !== undefined) {
107
+ delete formItemProps.label;
108
+ }
109
+
110
+ return {
111
+ path: resolveFieldIdentity(field),
112
+ gridItemProps: gridItemProps as GridItemProps,
113
+ formItemProps: formItemProps as any,
114
+ formItemSlots,
115
+ componentProps,
116
+ formItemContainer: container,
117
+ rawField: field,
118
+ };
119
+ }
120
+
121
+ return {
122
+ path: resolveFieldIdentity(field),
123
+ gridItemProps: gridItemProps as GridItemProps,
124
+ formItemProps: formItemProps as any,
125
+ formItemSlots,
126
+ componentProps,
127
+ rawField: field,
128
+ };
129
+ };
130
+
42
131
  /**
43
132
  * 将 fields 配置数组逐个拆分为 grid / formItem / component / slots 分组。
44
133
  *
45
134
  * @description
46
135
  * 1. 响应式集成 ProComponentProvider 的 `pro-form-item` 全局变量配置;
47
136
  * 2. 剥离 `extraProps`(用户自定义字段标识袋):保留在字段原始对象上供业务层存取,渲染层经解构剥离,不落入 componentProps 透传。
137
+ *
138
+ * @param {ComputedRef<Field[]>} validFields - 已过滤隐藏字段的字段配置数组
139
+ * @returns 字段渲染分组数组
48
140
  */
49
141
  export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
50
142
  const config = INJECT_CONFIG['pro-form-item'];
@@ -52,89 +144,23 @@ export function useGroupFieldAttrs(validFields: ComputedRef<Field[]>) {
52
144
 
53
145
  return computed<GroupedField[]>(() => {
54
146
  const rawInject = toValue(injectFormItemProps) || {};
55
- return validFields.value.map(field => {
56
- let gridItemProps: Record<string, any> = {};
57
- let formItemProps: Record<string, any> = {};
58
- const componentProps: Record<string, any> = { slots: {} };
59
- const formItemSlots: Record<string, any> = {};
60
-
61
- if (isPlainObject(field)) {
62
- // prettier-ignore
63
- const { path: _path, name: _name, fields, formItemClass, formItemStyle, hidden, span, getFormItemRef, getComponentRef, getFormItemComputedProps, getComponentComputedProps, slots = {}, formItemDataAttrs = {}, componentDataAttrs = {}, extraProps, formItemContainer, ...rest } =
64
- field as any;
65
-
66
- // prettier-ignore
67
- const { class: injectClassName, style: injectStyle, span: injectSpan, xs, sm, md, lg, xl, xxl, formItemContainer: injectFormItemContainer, ...injectRest } = rawInject;
68
-
69
- /*
70
- * 字段显式指定 span 时整体接管栅格,不再叠加注入的响应式断点;
71
- * 未指定 span 时使用注入的断点配置
72
- */
73
- const initGridItemProps =
74
- span || span === 0
75
- ? { span }
76
- : { xs, sm, md, lg, xl, xxl, span: injectSpan };
77
-
78
- // 分组字段未指定 span 时整行渲染(span 兜底 24),注入的响应式断点保持生效
79
- gridItemProps = fields
80
- ? { ...initGridItemProps, span: span ?? 24 }
81
- : initGridItemProps;
82
- // 字段级容器优先,回退注入级配置
83
- const container = formItemContainer ?? injectFormItemContainer;
84
- formItemProps = mergeProps(
85
- injectRest,
86
- { class: injectClassName, style: injectStyle },
87
- { class: toValue(formItemClass), style: toValue(formItemStyle) },
88
- formItemDataAttrs
89
- );
90
-
91
- Object.keys(rest).forEach(k => {
92
- if (gridItemPropKeySet.has(k)) {
93
- gridItemProps[k] = rest[k];
94
- } else if (formItemPropKeySet.has(k)) {
95
- formItemProps[k] = rest[k];
96
- } else {
97
- componentProps[k] = rest[k];
98
- }
99
- });
100
-
101
- Object.assign(componentProps, componentDataAttrs);
102
- Object.keys(slots).forEach(k => {
103
- if (formItemSlotKeySet.has(k)) {
104
- formItemSlots[k] = slots[k];
105
- } else {
106
- componentProps.slots[k] = slots[k];
107
- }
108
- });
109
-
110
- // label 插槽与字段 label 同时存在时由插槽渲染,剥离 label 属性
111
- if (formItemSlots.label && formItemProps.label !== undefined) {
112
- delete formItemProps.label;
113
- }
114
-
115
- const raw = resolveFieldIdentity(field);
116
- return {
117
- path: raw,
118
- gridItemProps: gridItemProps as GridItemProps,
119
- formItemProps: formItemProps as any,
120
- formItemSlots,
121
- componentProps,
122
- formItemContainer: container,
123
- rawField: field,
124
- renderKey: getFieldKey(field),
125
- };
126
- }
127
-
128
- const raw = resolveFieldIdentity(field);
129
- return {
130
- path: raw,
131
- gridItemProps: gridItemProps as GridItemProps,
132
- formItemProps: formItemProps as any,
133
- formItemSlots,
134
- componentProps,
135
- rawField: field,
136
- renderKey: getFieldKey(field),
137
- };
138
- });
147
+ return validFields.value.map(field => groupField(field, rawInject));
139
148
  });
140
149
  }
150
+
151
+ /**
152
+ * 将单个字段配置拆分为 grid / formItem / component / slots 分组
153
+ *
154
+ * @description 拆分规则与 `useGroupFieldAttrs` 一致,结果依赖该字段自身配置与 Provider 的 `pro-form-item` 默认值。
155
+ *
156
+ * @param {MaybeRefOrGetter<Field>} field - 待拆分的字段配置
157
+ * @returns {ComputedRef<GroupedField>} 单字段渲染分组
158
+ */
159
+ export function useGroupedField(field: MaybeRefOrGetter<Field>) {
160
+ const config = INJECT_CONFIG['pro-form-item'];
161
+ const injectFormItemProps = inject(config.injectionKey, config.default);
162
+
163
+ return computed<GroupedField>(() =>
164
+ groupField(toValue(field), toValue(injectFormItemProps) || {})
165
+ );
166
+ }
@@ -1,8 +1,26 @@
1
1
  import { toRaw } from 'vue';
2
+ import { resolveFieldIdentity, toPath } from '../../../shared/core';
2
3
 
3
4
  const fieldKeyMap = new WeakMap<object, number>();
4
5
  let fieldKeySeed = 0;
5
6
 
7
+ /** 数据行对象身份编号 */
8
+ const rowKeyMap = new WeakMap<object, number>();
9
+ let rowKeySeed = 0;
10
+
11
+ const getRowKey = (row: object) => {
12
+ const raw = toRaw(row);
13
+ let key = rowKeyMap.get(raw);
14
+ if (key === undefined) {
15
+ key = ++rowKeySeed;
16
+ rowKeyMap.set(raw, key);
17
+ }
18
+ return key;
19
+ };
20
+
21
+ /** 数组下标段判定:仅十进制非负整数段按数组下标处理 */
22
+ const isIndexSegment = (segment: string) => /^(?:0|[1-9]\d*)$/.test(segment);
23
+
6
24
  /**
7
25
  * 字段渲染 key
8
26
  *
@@ -25,3 +43,48 @@ export const getFieldKey = (field: unknown): string | number => {
25
43
  }
26
44
  return key;
27
45
  };
46
+
47
+ /**
48
+ * 字段的数据行渲染 key
49
+ *
50
+ * @description 沿字段路径逐段解析原始表单数据,取路径经过的对象数组行元素身份与行内剩余路径组成 key;
51
+ * 同一数据行对象在数组增删 / 重排后恒得同一 key。路径不经过对象数组行、行元素为原始值或数据缺失时,
52
+ * 回退到 {@link getFieldKey} 的字段对象身份。同 key 的去重由调用方按兄弟列表收敛。
53
+ *
54
+ * @param field - 字段配置项
55
+ * @param rawFormData - 原始表单数据(非响应式对象)
56
+ * @returns 数据行 key;无法定位数据行时返回字段对象身份 key
57
+ */
58
+ export const getFieldDataKey = (
59
+ field: unknown,
60
+ rawFormData: unknown
61
+ ): string | number => {
62
+ const path = resolveFieldIdentity(field);
63
+ if (!path || rawFormData === null || rawFormData === undefined) {
64
+ return getFieldKey(field);
65
+ }
66
+
67
+ const segments = toPath(path);
68
+ let node: any = rawFormData;
69
+ const rowIds: number[] = [];
70
+ let lastRowIndex = -1;
71
+
72
+ for (let i = 0; i < segments.length; i++) {
73
+ if (Array.isArray(node)) {
74
+ const segment = segments[i];
75
+ if (!isIndexSegment(segment)) break;
76
+ const element = node[Number(segment)];
77
+ if (element === null || typeof element !== 'object') break;
78
+ rowIds.push(getRowKey(toRaw(element) as object));
79
+ lastRowIndex = i;
80
+ node = element;
81
+ continue;
82
+ }
83
+ if (node === null || typeof node !== 'object') break;
84
+ node = node[segments[i]];
85
+ }
86
+
87
+ if (lastRowIndex === -1) return getFieldKey(field);
88
+ const rest = segments.slice(lastRowIndex + 1).join('.');
89
+ return `data:${rowIds.join('/')}:${rest}`;
90
+ };
@@ -46,6 +46,7 @@
46
46
  * @param {boolean | Partial<Control>} [control] - 是否展示尺寸与列控制按钮;显式传入 columns 时列控入口不渲染
47
47
  * @param {SearchFormConfig} [searchFormConfig] - 搜索栏查询字段表单配置
48
48
  * @param {ContainerComponent | false} [tableContainer] - 表格区域包裹容器
49
+ * @param {boolean | EllipsisTooltipConfig} [ellipsisTooltip] - truthy 时全局启用所有列;对象同时作为 Tooltip 配置;列上 `showTooltip: false` 或 `ellipsis: false` 可退出
49
50
  *
50
51
  * @slot search-form - 搜索区域插槽,作用域参数 form / search / reset
51
52
  * @slot button-bar - 表格头部按钮区插槽
@@ -65,7 +66,9 @@ import {
65
66
  nextTick,
66
67
  onMounted,
67
68
  inject,
69
+ ref,
68
70
  toValue,
71
+ watch,
69
72
  type Slot,
70
73
  } from 'vue';
71
74
  import { INJECT_CONFIG } from '../../component-provider/constants';
@@ -91,7 +94,15 @@ import DefaultSearchFormContainer from './DefaultSearchFormContainer.vue';
91
94
  import DefaultTableContainer from './DefaultTableContainer.vue';
92
95
  import SizeControl from './SizeControl.vue';
93
96
  import ColumnControl from './ColumnControl.vue';
94
- import type { Column, Columns, ColumnControlConfig } from '../types';
97
+ import TableEllipsisTooltip from './TableEllipsisTooltip.vue';
98
+ import { useEllipsisTooltip } from '../hooks/useEllipsisTooltip';
99
+ import { useEllipsisTooltipColumns } from '../hooks/useEllipsisTooltipColumns';
100
+ import type {
101
+ Column,
102
+ Columns,
103
+ ColumnControlConfig,
104
+ EllipsisTooltipConfig,
105
+ } from '../types';
95
106
 
96
107
  defineOptions({ name: 'ProTable', inheritAttrs: false });
97
108
 
@@ -124,6 +135,8 @@ type Props = {
124
135
  searchFormConfig?: SearchFormConfig;
125
136
  /** 表格区域包裹容器 */
126
137
  tableContainer?: ContainerComponent | false;
138
+ /** 表格级省略 Tooltip 配置;truthy 时全局启用 */
139
+ ellipsisTooltip?: boolean | EllipsisTooltipConfig;
127
140
  } & /* @vue-ignore */ TableProps;
128
141
 
129
142
  const props = withDefaults(defineProps<Props>(), {
@@ -134,6 +147,7 @@ const props = withDefaults(defineProps<Props>(), {
134
147
  control: undefined,
135
148
  searchFormConfig: undefined,
136
149
  tableContainer: undefined,
150
+ ellipsisTooltip: undefined,
137
151
  });
138
152
 
139
153
  const { t } = useLocale();
@@ -171,6 +185,8 @@ const searchFormProps = computed(() => {
171
185
 
172
186
  const attrs: Record<string, any> = useReactiveAttrs();
173
187
 
188
+ const rootRef = ref<HTMLElement | null>(null);
189
+
174
190
  const size = defineModel<TableProps['size']>('size');
175
191
  const loading = defineModel<boolean>('loading', { default: false });
176
192
 
@@ -318,25 +334,6 @@ const effectiveSize = computed<TableProps['size']>(() => {
318
334
  return size.value ?? injectAttrs.value.size;
319
335
  });
320
336
 
321
- /*
322
- * 下发给宿主表格的属性:Provider 注入为底(其不可注入键已在注入层剔除),调用方绑定与 v-model 透传值覆盖同键。
323
- * size / loading 为声明模型,其 onUpdate:* 回写通道不进入 attrs,无需另行剔除
324
- */
325
- const computedTableProps = computed<TableProps>(() => {
326
- const merged = {
327
- ...injectAttrs.value,
328
- ...omitBy(
329
- pick(camelAttrs.value, tableProperties),
330
- value => value === undefined
331
- ),
332
- } as Record<string, any>;
333
- // onChange 由模板监听器收口,散落 attrs 中的同名监听器并入会形成重复回调
334
- delete merged.onChange;
335
- merged.size = effectiveSize.value;
336
- merged.loading = undefined;
337
- return merged as TableProps;
338
- });
339
-
340
337
  const computedTableContainer = computed(() => {
341
338
  const container =
342
339
  props.tableContainer ??
@@ -378,45 +375,93 @@ const computedColumns = computed<Columns<RecordType>>(() => {
378
375
  return needIndex ? [indexColumn.value, ...cols] : cols;
379
376
  });
380
377
 
378
+ const ellipsisTooltipValue = computed(
379
+ () => props.ellipsisTooltip ?? injectProps.value.ellipsisTooltip
380
+ );
381
+ const ellipsisTooltipConfig = computed<EllipsisTooltipConfig>(() =>
382
+ typeof ellipsisTooltipValue.value === 'object' && ellipsisTooltipValue.value
383
+ ? ellipsisTooltipValue.value
384
+ : {}
385
+ );
386
+ const ellipsisTooltipGlobal = computed(() =>
387
+ Boolean(ellipsisTooltipValue.value)
388
+ );
389
+
390
+ const {
391
+ columns: visibleComputedColumns,
392
+ hasTooltip: ellipsisTooltipEnabled,
393
+ resolveOriginalColumn,
394
+ resolveSorterColumn,
395
+ } = useEllipsisTooltipColumns(
396
+ () => computedColumns.value,
397
+ () => ellipsisTooltipGlobal.value
398
+ );
399
+
381
400
  /*
382
- * 递归过滤 hidden 列(含分组子列):仅对被过滤过子列的列生成浅拷贝,
383
- * 未发生变动的列保持原对象引用(每次 columns 变化执行一次,不在渲染期递归)
401
+ * 下发给宿主表格的属性:Provider 注入为底(其不可注入键已在注入层剔除),调用方绑定与 v-model 透传值覆盖同键。
402
+ * size / loading 为声明模型,其 onUpdate:* 回写通道不进入 attrs,无需另行剔除
384
403
  */
385
- const filterVisibleColumns = (
386
- columns: any[],
387
- ancestors: Set<object> = new Set()
388
- ): { list: any[]; changed: boolean } => {
389
- const list: any[] = [];
390
- let changed = false;
391
- for (const column of columns) {
392
- if (!isObjectItem(column) || toValue((column as Column).hidden)) {
393
- changed = true;
394
- continue;
395
- }
396
- if (Array.isArray(column.children) && column.children.length > 0) {
397
- // 环状分组列不再深入,重复出现的祖先引用直接剔除
398
- if (ancestors.has(column)) {
399
- changed = true;
400
- continue;
401
- }
402
- ancestors.add(column);
403
- const filtered = filterVisibleColumns(column.children, ancestors);
404
- ancestors.delete(column);
405
- if (filtered.changed) {
406
- changed = true;
407
- list.push({ ...column, children: filtered.list });
408
- continue;
409
- }
410
- }
411
- list.push(column);
404
+ const computedTableProps = computed<TableProps>(() => {
405
+ const merged = {
406
+ ...injectAttrs.value,
407
+ ...omitBy(
408
+ pick(camelAttrs.value, tableProperties),
409
+ value => value === undefined
410
+ ),
411
+ } as Record<string, any>;
412
+ // onChange 由模板监听器收口,散落 attrs 中的同名监听器并入会形成重复回调
413
+ delete merged.onChange;
414
+ // resizeColumn 回传转换列时,用户回调仍操作原始列
415
+ const resizeColumn = merged.onResizeColumn;
416
+ if (typeof resizeColumn === 'function') {
417
+ merged.onResizeColumn = (width: number, column: any) =>
418
+ resizeColumn(width, resolveOriginalColumn(column));
412
419
  }
413
- return { list, changed };
414
- };
420
+ merged.size = effectiveSize.value;
421
+ merged.loading = undefined;
422
+ return merged as TableProps;
423
+ });
415
424
 
416
- const visibleComputedColumns = computed(() => {
417
- return filterVisibleColumns(computedColumns.value).list;
425
+ const ellipsisTooltipContainer = computed(() => {
426
+ const getPopupContainer = ellipsisTooltipConfig.value.getPopupContainer;
427
+ if (!getPopupContainer) return undefined;
428
+ return () => {
429
+ if (typeof document === 'undefined') return undefined;
430
+ const root = rootRef.value ?? document.body;
431
+ return getPopupContainer(root) ?? document.body;
432
+ };
433
+ });
434
+ const ellipsisTooltipDelay = computed(() =>
435
+ Math.max(0, (ellipsisTooltipConfig.value.mouseEnterDelay ?? 0.2) * 1000)
436
+ );
437
+ const ellipsisTooltipLeaveDelay = computed(() =>
438
+ Math.max(0, (ellipsisTooltipConfig.value.mouseLeaveDelay ?? 0.1) * 1000)
439
+ );
440
+ const ellipsisTooltipProps = computed(() => {
441
+ const config = ellipsisTooltipConfig.value;
442
+ const {
443
+ getPopupContainer: _getPopupContainer,
444
+ mouseEnterDelay: _mouseEnterDelay,
445
+ mouseLeaveDelay: _mouseLeaveDelay,
446
+ ...rest
447
+ } = config;
448
+ return rest;
418
449
  });
419
450
 
451
+ const {
452
+ open: ellipsisTooltipOpen,
453
+ content: ellipsisTooltipContent,
454
+ anchorStyle: ellipsisTooltipAnchorStyle,
455
+ onMouseOver: onEllipsisMouseOver,
456
+ onMouseOut: onEllipsisMouseOut,
457
+ close: closeEllipsisTooltip,
458
+ } = useEllipsisTooltip(
459
+ rootRef,
460
+ ellipsisTooltipEnabled,
461
+ ellipsisTooltipDelay,
462
+ ellipsisTooltipLeaveDelay
463
+ );
464
+
420
465
  // 数据源优先级:显式绑定 > table 实例(useTable 创建的 dataSource)
421
466
  const computedDataSource = computed<RecordType[]>(() => {
422
467
  return (
@@ -424,6 +469,9 @@ const computedDataSource = computed<RecordType[]>(() => {
424
469
  );
425
470
  });
426
471
 
472
+ // 列或数据变化后旧锚点失效,关闭共享 Tooltip
473
+ watch([visibleComputedColumns, computedDataSource], closeEllipsisTooltip);
474
+
427
475
  /*
428
476
  * 分页优先级:显式绑定 > provider 全局配置 > 默认合成;
429
477
  * provider 作为基础配置与 table.pageParam 合并,显式绑定的对象形态在其之上覆盖
@@ -479,8 +527,15 @@ const onTableChange: TableProps['onChange'] = (...args) => {
479
527
  );
480
528
  if (shadowed.length) warnPaginationShadow(shadowed);
481
529
  }
482
- // change 回放:模板监听器优先,未绑定时回退 Provider 全局配置
483
- (attrs.onChange ?? injectProps.value.onChange)?.(...args);
530
+ // change 回放:模板监听器优先,未绑定时回退 Provider 全局配置;sorter.column 还原为原始列
531
+ const nextArgs = [...args] as any[];
532
+ const sorterArg = nextArgs[2];
533
+ if (Array.isArray(sorterArg)) {
534
+ nextArgs[2] = sorterArg.map(resolveSorterColumn);
535
+ } else if (sorterArg) {
536
+ nextArgs[2] = resolveSorterColumn(sorterArg);
537
+ }
538
+ (attrs.onChange ?? injectProps.value.onChange)?.(...nextArgs);
484
539
  _search();
485
540
  };
486
541
 
@@ -541,7 +596,15 @@ defineExpose({
541
596
  </script>
542
597
 
543
598
  <template>
544
- <div class="pro-table" :class="attrs.class" :style="attrs.style">
599
+ <div
600
+ ref="rootRef"
601
+ class="pro-table"
602
+ data-pro-table-root
603
+ :class="attrs.class"
604
+ :style="attrs.style"
605
+ @mouseover="onEllipsisMouseOver"
606
+ @mouseout="onEllipsisMouseOut"
607
+ >
545
608
  <ContainerFragment
546
609
  v-if="searchFormVisible || $slots['search-form']"
547
610
  :component="computedSearchFormConfig.container"
@@ -620,6 +683,15 @@ defineExpose({
620
683
  </ATable>
621
684
  </slot>
622
685
  </ContainerFragment>
686
+
687
+ <TableEllipsisTooltip
688
+ v-if="ellipsisTooltipEnabled"
689
+ :open="ellipsisTooltipOpen"
690
+ :content="ellipsisTooltipContent"
691
+ :anchor-style="ellipsisTooltipAnchorStyle"
692
+ :container="ellipsisTooltipContainer"
693
+ v-bind="ellipsisTooltipProps"
694
+ />
623
695
  </div>
624
696
  </template>
625
697