@qin-ui/antd-vue-pro 2.5.3 → 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 (30) hide show
  1. package/AI-CONTEXT.md +30 -10
  2. package/api.json +23 -3
  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/table/components/ProTable.vue.d.ts +3 -1
  7. package/es/components/table/components/ProTable.vue.d.ts.map +1 -1
  8. package/es/components/table/components/TableEllipsisTooltip.vue.d.ts +19 -0
  9. package/es/components/table/components/TableEllipsisTooltip.vue.d.ts.map +1 -0
  10. package/es/components/table/hooks/useEllipsisTooltip.d.ts +45 -0
  11. package/es/components/table/hooks/useEllipsisTooltip.d.ts.map +1 -0
  12. package/es/components/table/hooks/useEllipsisTooltipColumns.d.ts +19 -0
  13. package/es/components/table/hooks/useEllipsisTooltipColumns.d.ts.map +1 -0
  14. package/es/components/table/index.d.ts +1 -1
  15. package/es/components/table/index.d.ts.map +1 -1
  16. package/es/components/table/types/index.d.ts +46 -2
  17. package/es/components/table/types/index.d.ts.map +1 -1
  18. package/es/index.js +438 -87
  19. package/es/index.js.map +1 -1
  20. package/es/shared/ui/index.d.ts +2 -1
  21. package/es/shared/ui/index.d.ts.map +1 -1
  22. package/package.json +2 -2
  23. package/src/components/component-provider/types/index.ts +1 -0
  24. package/src/components/table/components/ProTable.vue +128 -56
  25. package/src/components/table/components/TableEllipsisTooltip.vue +90 -0
  26. package/src/components/table/hooks/useEllipsisTooltip.ts +244 -0
  27. package/src/components/table/hooks/useEllipsisTooltipColumns.ts +139 -0
  28. package/src/components/table/index.ts +7 -1
  29. package/src/components/table/types/index.ts +62 -2
  30. package/src/shared/ui/index.ts +2 -0
@@ -1,8 +1,9 @@
1
1
  import { FormProps as _FormProps, Table, TableProps as _TableProps } from 'ant-design-vue';
2
2
  import { ComponentProps } from 'vue-component-type-helpers';
3
3
  import { Options } from 'scroll-into-view-if-needed';
4
- export { theme, Form, FormItem, Row as Grid, Col as GridItem, Table, Space, MenuDivider, Dropdown, Menu, MenuItem, Checkbox, Button, Cascader, CheckboxGroup, DatePicker, Input, InputNumber, InputPassword, InputSearch, RadioGroup, Select, Slider, Switch, Textarea, TimePicker, Transfer, TreeSelect, RangePicker, type FormItemInstance, type FormInstance, type RowProps as GridProps, type ColProps as GridItemProps, type FormItemProps, type PaginationProps, } from 'ant-design-vue';
4
+ export { theme, Form, FormItem, Row as Grid, Col as GridItem, Table, Space, MenuDivider, Dropdown, Menu, MenuItem, Checkbox, Button, Cascader, CheckboxGroup, DatePicker, Input, InputNumber, InputPassword, InputSearch, RadioGroup, Select, Slider, Switch, Textarea, TimePicker, Transfer, TreeSelect, RangePicker, Tooltip, type FormItemInstance, type FormInstance, type RowProps as GridProps, type ColProps as GridItemProps, type FormItemProps, type PaginationProps, } from 'ant-design-vue';
5
5
  export { type ColumnType, tableProps } from 'ant-design-vue/es/table';
6
+ export type { TooltipProps } from 'ant-design-vue';
6
7
  export { type RangePickerProps } from 'ant-design-vue/es/date-picker';
7
8
  export { useConfigContextInject } from 'ant-design-vue/es/config-provider/context';
8
9
  export { useInjectDisabled, useProviderDisabled, } from 'ant-design-vue/es/config-provider/DisabledContext';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,IAAI,UAAU,EACvB,KAAK,EACL,UAAU,IAAI,WAAW,EAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,OAAO,EACL,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,GAAG,IAAI,IAAI,EACX,GAAG,IAAI,QAAQ,EACf,KAAK,EACL,KAAK,EACL,WAAW,EACX,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,aAAa,EACb,UAAU,EACV,KAAK,EACL,WAAW,EACX,aAAa,EACb,WAAW,EACX,UAAU,EACV,MAAM,EACN,MAAM,EACN,MAAM,EACN,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,UAAU,EACV,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,QAAQ,IAAI,SAAS,EAC1B,KAAK,QAAQ,IAAI,aAAa,EAC9B,KAAK,aAAa,EAClB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,KAAK,UAAU,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACnF,OAAO,EACL,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,EAAE,QAAQ,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEzE,oDAAoD;AACpD,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC,GAAG;IAC/D,kBAAkB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CAC7C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,0BAA0B,CAAC,EAAE,cAAc,CACzC,OAAO,KAAK,CACb,CAAC,0BAA0B,CAAC,CAAC;CAC/B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,IAAI,UAAU,EACvB,KAAK,EACL,UAAU,IAAI,WAAW,EAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,OAAO,EACL,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,GAAG,IAAI,IAAI,EACX,GAAG,IAAI,QAAQ,EACf,KAAK,EACL,KAAK,EACL,WAAW,EACX,QAAQ,EACR,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,aAAa,EACb,UAAU,EACV,KAAK,EACL,WAAW,EACX,aAAa,EACb,WAAW,EACX,UAAU,EACV,MAAM,EACN,MAAM,EACN,MAAM,EACN,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,UAAU,EACV,WAAW,EACX,OAAO,EACP,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,QAAQ,IAAI,SAAS,EAC1B,KAAK,QAAQ,IAAI,aAAa,EAC9B,KAAK,aAAa,EAClB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,KAAK,UAAU,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACnF,OAAO,EACL,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,EAAE,QAAQ,IAAI,aAAa,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAEzE,oDAAoD;AACpD,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC,GAAG;IAC/D,kBAAkB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CAC7C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,0BAA0B,CAAC,EAAE,cAAc,CACzC,OAAO,KAAK,CACb,CAAC,0BAA0B,CAAC,CAAC;CAC/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qin-ui/antd-vue-pro",
3
- "version": "2.5.3",
3
+ "version": "2.6.0",
4
4
  "description": "二次封装antd vue组件",
5
5
  "bin": {
6
6
  "antd-vue-pro": "./bin/init-ai.mjs"
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "scroll-into-view-if-needed": "^3.1.0",
46
46
  "vue-component-type-helpers": "^3.2.5",
47
- "@qin-ui/pro-components-core": "^2.1.1"
47
+ "@qin-ui/pro-components-core": "^2.1.2"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^24",
@@ -58,6 +58,7 @@ export type RequiredComponentVars = {
58
58
  | 'immediateSearch'
59
59
  | 'addIndexColumn'
60
60
  | 'tableContainer'
61
+ | 'ellipsisTooltip'
61
62
  >
62
63
  >;
63
64
  /** ProForm 全局默认属性,grid 必须为 GridProps 对象而非 boolean */
@@ -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
 
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 表格级共享省略 Tooltip
4
+ *
5
+ * @component TableEllipsisTooltip
6
+ * @description 以固定定位锚点承载单个宿主 Tooltip,锚点位置由 ProTable 按悬停单元格更新;
7
+ * 整个表格只渲染一个 Tooltip 实例,实例数量不随单元格数量增长。
8
+ */
9
+ import { nextTick, ref, useAttrs, watch, type Component } from 'vue';
10
+ import { Tooltip as ATooltip } from '../../../shared/ui';
11
+ import type { EllipsisTooltipAnchorStyle } from '../hooks/useEllipsisTooltip';
12
+ import type { EllipsisTooltipConfig } from '../types';
13
+
14
+ defineOptions({ name: 'TableEllipsisTooltip', inheritAttrs: false });
15
+
16
+ type HostTooltipProps = Omit<
17
+ EllipsisTooltipConfig,
18
+ 'getPopupContainer' | 'mouseEnterDelay' | 'mouseLeaveDelay'
19
+ >;
20
+
21
+ type Props = {
22
+ open?: boolean;
23
+ content?: string;
24
+ anchorStyle?: EllipsisTooltipAnchorStyle;
25
+ container?: () => HTMLElement | null | undefined;
26
+ } & /* @vue-ignore */ HostTooltipProps;
27
+
28
+ const props = withDefaults(defineProps<Props>(), {
29
+ open: false,
30
+ content: '',
31
+ anchorStyle: () => ({
32
+ position: 'fixed',
33
+ pointerEvents: 'none',
34
+ opacity: 0,
35
+ }),
36
+ container: () => undefined,
37
+ });
38
+ const attrs = useAttrs() as HostTooltipProps;
39
+ const HostTooltip = ATooltip as unknown as Component;
40
+
41
+ type TooltipInstance = {
42
+ forceAlign?: () => void;
43
+ forcePopupAlign?: () => void;
44
+ };
45
+
46
+ const tooltipRef = ref<TooltipInstance | null>(null);
47
+
48
+ /** 锚点或内容变化后重算宿主浮层位置 */
49
+ const forceAlign = async () => {
50
+ if (!props.open) return;
51
+ await nextTick();
52
+ const instance = tooltipRef.value;
53
+ (instance?.forceAlign ?? instance?.forcePopupAlign)?.();
54
+ };
55
+
56
+ watch(() => props.anchorStyle, forceAlign, { deep: true });
57
+ watch(
58
+ () => props.open,
59
+ value => {
60
+ if (value) forceAlign();
61
+ }
62
+ );
63
+
64
+ const getContainerNode = (): HTMLElement | undefined => {
65
+ const container = props.container?.();
66
+ if (container) return container;
67
+ return typeof document !== 'undefined' ? document.body : undefined;
68
+ };
69
+
70
+ const getPopupContainer = () => getContainerNode() ?? document.body;
71
+ </script>
72
+
73
+ <template>
74
+ <Teleport :to="getContainerNode() ?? 'body'">
75
+ <HostTooltip
76
+ v-bind="attrs"
77
+ ref="tooltipRef"
78
+ :open="open"
79
+ :title="content"
80
+ :trigger="[]"
81
+ :get-popup-container="getPopupContainer"
82
+ >
83
+ <span
84
+ class="pro-table_ellipsis-tooltip-anchor"
85
+ :style="anchorStyle"
86
+ aria-hidden="true"
87
+ />
88
+ </HostTooltip>
89
+ </Teleport>
90
+ </template>
@@ -0,0 +1,244 @@
1
+ import {
2
+ onBeforeUnmount,
3
+ onDeactivated,
4
+ onMounted,
5
+ ref,
6
+ watch,
7
+ type Ref,
8
+ } from 'vue';
9
+
10
+ /** 省略单元格标记属性,由列转换写入 body 单元格 */
11
+ export const ELLIPSIS_TOOLTIP_CELL_ATTR = 'data-pro-table-ellipsis';
12
+
13
+ /** ProTable 根节点归属标记 */
14
+ export const PRO_TABLE_ROOT_ATTR = 'data-pro-table-root';
15
+
16
+ /** 共享 Tooltip 锚点样式 */
17
+ export type EllipsisTooltipAnchorStyle = {
18
+ position: 'fixed';
19
+ pointerEvents: 'none';
20
+ opacity: number;
21
+ left?: string;
22
+ top?: string;
23
+ width?: string;
24
+ height?: string;
25
+ };
26
+
27
+ /** 共享 Tooltip 状态与事件处理器 */
28
+ export type EllipsisTooltipState = {
29
+ /** Tooltip 是否展示 */
30
+ open: Ref<boolean>;
31
+ /** Tooltip 文本 */
32
+ content: Ref<string>;
33
+ /** 固定定位锚点样式 */
34
+ anchorStyle: Ref<EllipsisTooltipAnchorStyle>;
35
+ /** 根节点 mouseover 处理器 */
36
+ onMouseOver: (event: MouseEvent) => void;
37
+ /** 根节点 mouseout 处理器 */
38
+ onMouseOut: (event: MouseEvent) => void;
39
+ /** 关闭 Tooltip */
40
+ close: () => void;
41
+ };
42
+
43
+ /** 共享 Tooltip 锚点的基础样式:固定定位,不参与命中测试与绘制 */
44
+ const ANCHOR_BASE_STYLE: EllipsisTooltipAnchorStyle = {
45
+ position: 'fixed',
46
+ pointerEvents: 'none',
47
+ opacity: 0,
48
+ };
49
+
50
+ /** 读取单元格可读文本,连续空白折叠为单个空格 */
51
+ const getCellText = (cell: HTMLElement) => {
52
+ const text = cell.innerText || cell.textContent || '';
53
+ return text.replace(/\s+/g, ' ').trim();
54
+ };
55
+
56
+ /** 单元格测量结果:文本与是否横向溢出 */
57
+ type CellMeasurement = {
58
+ text: string;
59
+ overflow: boolean;
60
+ };
61
+
62
+ /** 单次读取单元格文本并判断横向溢出 */
63
+ const measureCell = (cell: HTMLElement): CellMeasurement => {
64
+ const text = getCellText(cell);
65
+ return {
66
+ text,
67
+ overflow: Boolean(text) && cell.scrollWidth > cell.clientWidth,
68
+ };
69
+ };
70
+
71
+ /**
72
+ * 表格级共享省略 Tooltip
73
+ *
74
+ * @description
75
+ * 在 ProTable 根节点上以事件委托识别带标记的 body 单元格,维护单个 Tooltip 的显隐、文本与固定定位锚点;
76
+ * 仅在单元格文本横向溢出时触发;表格滚动、窗口尺寸变化、KeepAlive 停用时关闭,卸载时移除监听。
77
+ *
78
+ * @param rootRef - ProTable 根节点引用
79
+ * @param enabled - 是否启用共享 Tooltip;为 false 时不注册全局监听
80
+ * @param delay - 悬停延迟(ms),0 表示立即展示
81
+ * @param leaveDelay - 移出延迟(ms),0 表示立即关闭
82
+ * @returns Tooltip 状态与根节点事件处理器
83
+ */
84
+ export function useEllipsisTooltip(
85
+ rootRef: Ref<HTMLElement | null | undefined>,
86
+ enabled: Ref<boolean>,
87
+ delay: Ref<number>,
88
+ leaveDelay: Ref<number>
89
+ ): EllipsisTooltipState {
90
+ const open = ref(false);
91
+ const content = ref('');
92
+ const anchorStyle = ref<EllipsisTooltipAnchorStyle>({
93
+ ...ANCHOR_BASE_STYLE,
94
+ });
95
+ let activeCell: HTMLElement | null = null;
96
+ let pendingCell: HTMLElement | null = null;
97
+ let openTimer: ReturnType<typeof setTimeout> | null = null;
98
+ let closeTimer: ReturnType<typeof setTimeout> | null = null;
99
+
100
+ const clearOpenTimer = () => {
101
+ if (openTimer === null) return;
102
+ clearTimeout(openTimer);
103
+ openTimer = null;
104
+ pendingCell = null;
105
+ };
106
+
107
+ const clearCloseTimer = () => {
108
+ if (closeTimer === null) return;
109
+ clearTimeout(closeTimer);
110
+ closeTimer = null;
111
+ };
112
+
113
+ const close = () => {
114
+ clearOpenTimer();
115
+ clearCloseTimer();
116
+ open.value = false;
117
+ activeCell = null;
118
+ };
119
+
120
+ const getEventRoot = (node: Node | null) => {
121
+ const element = node instanceof Element ? node : node?.parentElement;
122
+ return element?.closest?.(`[${PRO_TABLE_ROOT_ATTR}]`) ?? null;
123
+ };
124
+
125
+ const getCellFromEvent = (event: MouseEvent) => {
126
+ const target = event.target as Element | null;
127
+ const cell = target?.closest?.(`[${ELLIPSIS_TOOLTIP_CELL_ATTR}]`);
128
+ if (!(cell instanceof HTMLElement)) return null;
129
+ const root = rootRef.value;
130
+ if (!root) return null;
131
+ if (cell.closest(`[${PRO_TABLE_ROOT_ATTR}]`) !== root) return null;
132
+ return cell;
133
+ };
134
+
135
+ const onMouseOver = (event: MouseEvent) => {
136
+ const cell = getCellFromEvent(event);
137
+ if (!cell) return;
138
+ clearCloseTimer();
139
+ if (cell === activeCell || cell === pendingCell) return;
140
+ close();
141
+ const measurement = measureCell(cell);
142
+ if (!measurement.overflow) return;
143
+ const rect = cell.getBoundingClientRect();
144
+ if (!rect.width || !rect.height) return;
145
+ content.value = measurement.text;
146
+ anchorStyle.value = {
147
+ ...ANCHOR_BASE_STYLE,
148
+ left: `${rect.left}px`,
149
+ top: `${rect.top}px`,
150
+ width: `${rect.width}px`,
151
+ height: `${rect.height}px`,
152
+ };
153
+ pendingCell = cell;
154
+ openTimer = setTimeout(
155
+ () => {
156
+ openTimer = null;
157
+ pendingCell = null;
158
+ if (!cell.isConnected) return;
159
+ activeCell = cell;
160
+ open.value = true;
161
+ },
162
+ Math.max(0, delay.value)
163
+ );
164
+ };
165
+
166
+ const onMouseOut = (event: MouseEvent) => {
167
+ if (!activeCell && !pendingCell) return;
168
+
169
+ const owner = activeCell ?? pendingCell;
170
+ const target = event.target as Node | null;
171
+ if (!owner || !target || !owner.contains(target)) return;
172
+
173
+ const related = event.relatedTarget as Node | null;
174
+ if (related && owner.contains(related)) {
175
+ const relatedRoot = getEventRoot(related);
176
+ if (!relatedRoot || relatedRoot === rootRef.value) return;
177
+ close();
178
+ return;
179
+ }
180
+
181
+ if (pendingCell) {
182
+ close();
183
+ return;
184
+ }
185
+
186
+ clearCloseTimer();
187
+ const delayMs = Math.max(0, leaveDelay.value);
188
+ if (delayMs === 0) {
189
+ close();
190
+ return;
191
+ }
192
+ closeTimer = setTimeout(() => {
193
+ closeTimer = null;
194
+ close();
195
+ }, delayMs);
196
+ };
197
+
198
+ const onViewportChange = () => close();
199
+ let listening = false;
200
+
201
+ const addViewportListeners = () => {
202
+ if (listening || typeof window === 'undefined') return;
203
+ listening = true;
204
+ window.addEventListener('scroll', onViewportChange, true);
205
+ window.addEventListener('resize', onViewportChange);
206
+ };
207
+
208
+ const removeViewportListeners = () => {
209
+ if (!listening) return;
210
+ listening = false;
211
+ window.removeEventListener('scroll', onViewportChange, true);
212
+ window.removeEventListener('resize', onViewportChange);
213
+ };
214
+
215
+ onDeactivated(close);
216
+
217
+ onMounted(() => {
218
+ if (enabled.value) addViewportListeners();
219
+ });
220
+
221
+ watch(enabled, value => {
222
+ if (value) {
223
+ addViewportListeners();
224
+ } else {
225
+ removeViewportListeners();
226
+ close();
227
+ }
228
+ });
229
+
230
+ onBeforeUnmount(() => {
231
+ removeViewportListeners();
232
+ clearOpenTimer();
233
+ clearCloseTimer();
234
+ });
235
+
236
+ return {
237
+ open,
238
+ content,
239
+ anchorStyle,
240
+ onMouseOver,
241
+ onMouseOut,
242
+ close,
243
+ };
244
+ }