@qin-ui/antd-vue-pro 2.6.1 → 2.6.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qin-ui/antd-vue-pro",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "description": "二次封装antd vue组件",
5
5
  "bin": {
6
6
  "antd-vue-pro": "./bin/init-ai.mjs"
@@ -30,14 +30,14 @@
30
30
  * @param {...FormProps} ...attrs - Ant Design Vue Form 组件的其他属性
31
31
  *
32
32
  * @slot default - 默认插槽,可用于添加额外内容
33
- * @slot [path] - 以字段 path 命名的动态插槽,可用于自定义字段渲染
33
+ * @slot [path] - 以字段 path 命名的动态插槽,优先于 component 渲染;作用域含字段值 value、回写通道 onUpdate:value、disabled、path 与合并后的控件属性,可直接 v-bind 给控件
34
34
  *
35
35
  * @example
36
36
  * ```vue
37
37
  * <template>
38
38
  * <ProForm :form="form" @submit="handleSubmit">
39
- * <template #name="{ value, disabled }">
40
- * <a-input :value="value" :disabled="disabled" />
39
+ * <template #name="scoped">
40
+ * <a-input v-bind="scoped" />
41
41
  * </template>
42
42
  * </ProForm>
43
43
  * </template>
@@ -489,6 +489,11 @@ const ellipsisTooltipProps = computed(() => {
489
489
  return rest;
490
490
  });
491
491
 
492
+ const ellipsisTooltipOverlay = shallowRef<HTMLElement | null>(null);
493
+ const setEllipsisTooltipOverlay = (element: HTMLElement | null) => {
494
+ ellipsisTooltipOverlay.value = element;
495
+ };
496
+
492
497
  const {
493
498
  open: ellipsisTooltipOpen,
494
499
  content: ellipsisTooltipContent,
@@ -500,7 +505,8 @@ const {
500
505
  rootRef,
501
506
  ellipsisTooltipEnabled,
502
507
  ellipsisTooltipDelay,
503
- ellipsisTooltipLeaveDelay
508
+ ellipsisTooltipLeaveDelay,
509
+ ellipsisTooltipOverlay
504
510
  );
505
511
 
506
512
  // 数据源优先级:显式绑定 > table 实例(useTable 创建的 dataSource)
@@ -746,6 +752,7 @@ defineExpose({
746
752
  :anchor-style="ellipsisTooltipAnchorStyle"
747
753
  :container="ellipsisTooltipContainer"
748
754
  v-bind="ellipsisTooltipProps"
755
+ :set-overlay-element="setEllipsisTooltipOverlay"
749
756
  />
750
757
  </div>
751
758
  </template>
@@ -756,15 +763,12 @@ defineExpose({
756
763
  display: flex;
757
764
  align-items: center;
758
765
  justify-content: flex-end;
766
+ margin-bottom: 8px;
759
767
 
760
768
  &:empty {
761
769
  display: none;
762
770
  }
763
771
 
764
- & + &_content {
765
- margin-top: 16px;
766
- }
767
-
768
772
  &_button-bar {
769
773
  flex: 1;
770
774
  }
@@ -6,9 +6,20 @@
6
6
  * @description 以固定定位锚点承载单个宿主 Tooltip,锚点位置由 ProTable 按悬停单元格更新;
7
7
  * 整个表格只渲染一个 Tooltip 实例,实例数量不随单元格数量增长。
8
8
  */
9
- import { nextTick, ref, useAttrs, watch, type Component } from 'vue';
9
+ import {
10
+ nextTick,
11
+ onBeforeUnmount,
12
+ ref,
13
+ useAttrs,
14
+ watch,
15
+ type Component,
16
+ } from 'vue';
10
17
  import { Tooltip as ATooltip } from '../../../shared/ui';
11
- import type { EllipsisTooltipAnchorStyle } from '../hooks/useEllipsisTooltip';
18
+ import {
19
+ registerEllipsisTooltipOverlay,
20
+ unregisterEllipsisTooltipOverlay,
21
+ type EllipsisTooltipAnchorStyle,
22
+ } from '../hooks/useEllipsisTooltip';
12
23
  import type { EllipsisTooltipConfig } from '../types';
13
24
 
14
25
  defineOptions({ name: 'TableEllipsisTooltip', inheritAttrs: false });
@@ -23,6 +34,7 @@ type Props = {
23
34
  content?: string;
24
35
  anchorStyle?: EllipsisTooltipAnchorStyle;
25
36
  container?: () => HTMLElement | null | undefined;
37
+ setOverlayElement?: (element: HTMLElement | null) => void;
26
38
  } & /* @vue-ignore */ HostTooltipProps;
27
39
 
28
40
  const props = withDefaults(defineProps<Props>(), {
@@ -34,6 +46,7 @@ const props = withDefaults(defineProps<Props>(), {
34
46
  opacity: 0,
35
47
  }),
36
48
  container: () => undefined,
49
+ setOverlayElement: () => undefined,
37
50
  });
38
51
  const attrs = useAttrs() as HostTooltipProps;
39
52
  const HostTooltip = ATooltip as unknown as Component;
@@ -41,14 +54,58 @@ const HostTooltip = ATooltip as unknown as Component;
41
54
  type TooltipInstance = {
42
55
  forceAlign?: () => void;
43
56
  forcePopupAlign?: () => void;
57
+ getPopupDomNode?: () => HTMLElement | undefined;
44
58
  };
45
59
 
46
60
  const tooltipRef = ref<TooltipInstance | null>(null);
61
+ let currentOverlay: HTMLElement | null = null;
62
+ let overlaySyncToken = 0;
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
+ /** 读取宿主 Tooltip 浮层节点 */
71
+ const getPopupElement = (): HTMLElement | null =>
72
+ tooltipRef.value?.getPopupDomNode?.() ?? null;
73
+
74
+ /** 同步浮层节点引用与全局登记 */
75
+ const syncOverlayElement = (next: HTMLElement | null) => {
76
+ if (currentOverlay === next) return;
77
+ if (currentOverlay) unregisterEllipsisTooltipOverlay(currentOverlay);
78
+ currentOverlay = next;
79
+ props.setOverlayElement(next);
80
+ if (next) registerEllipsisTooltipOverlay(next);
81
+ };
82
+
83
+ /** 浮层渲染后同步节点引用,宿主延迟挂载时重试 */
84
+ const syncOverlayFromDom = async () => {
85
+ if (!props.open) return;
86
+ const token = ++overlaySyncToken;
87
+ for (let attempt = 0; attempt < 3; attempt += 1) {
88
+ const popup = getPopupElement();
89
+ if (popup) {
90
+ if (token === overlaySyncToken) syncOverlayElement(popup);
91
+ return;
92
+ }
93
+ if (!props.open) return;
94
+ await new Promise(resolve => setTimeout(resolve, 0));
95
+ }
96
+ if (token === overlaySyncToken) syncOverlayElement(null);
97
+ };
98
+
99
+ const clearOverlay = () => {
100
+ overlaySyncToken += 1;
101
+ syncOverlayElement(null);
102
+ };
47
103
 
48
104
  /** 锚点或内容变化后重算宿主浮层位置 */
49
105
  const forceAlign = async () => {
50
106
  if (!props.open) return;
51
107
  await nextTick();
108
+ syncOverlayFromDom();
52
109
  const instance = tooltipRef.value;
53
110
  (instance?.forceAlign ?? instance?.forcePopupAlign)?.();
54
111
  };
@@ -57,15 +114,15 @@ watch(() => props.anchorStyle, forceAlign, { deep: true });
57
114
  watch(
58
115
  () => props.open,
59
116
  value => {
60
- if (value) forceAlign();
117
+ if (value) {
118
+ forceAlign();
119
+ } else {
120
+ clearOverlay();
121
+ }
61
122
  }
62
123
  );
63
124
 
64
- const getContainerNode = (): HTMLElement | undefined => {
65
- const container = props.container?.();
66
- if (container) return container;
67
- return typeof document !== 'undefined' ? document.body : undefined;
68
- };
125
+ onBeforeUnmount(clearOverlay);
69
126
 
70
127
  const getPopupContainer = () => getContainerNode() ?? document.body;
71
128
  </script>
@@ -13,6 +13,19 @@ export const ELLIPSIS_TOOLTIP_CELL_ATTR = 'data-pro-table-ellipsis';
13
13
  /** ProTable 根节点归属标记 */
14
14
  export const PRO_TABLE_ROOT_ATTR = 'data-pro-table-root';
15
15
 
16
+ /** 当前打开中的省略 Tooltip 浮层 */
17
+ const activeOverlays = new Set<HTMLElement>();
18
+
19
+ /** 登记省略 Tooltip 浮层 */
20
+ export const registerEllipsisTooltipOverlay = (overlay: HTMLElement) => {
21
+ activeOverlays.add(overlay);
22
+ };
23
+
24
+ /** 注销省略 Tooltip 浮层 */
25
+ export const unregisterEllipsisTooltipOverlay = (overlay: HTMLElement) => {
26
+ activeOverlays.delete(overlay);
27
+ };
28
+
16
29
  /** 共享 Tooltip 锚点样式 */
17
30
  export type EllipsisTooltipAnchorStyle = {
18
31
  position: 'fixed';
@@ -68,24 +81,85 @@ const measureCell = (cell: HTMLElement): CellMeasurement => {
68
81
  };
69
82
  };
70
83
 
84
+ /** 单元格内容盒矩形 */
85
+ type ContentRect = {
86
+ left: number;
87
+ top: number;
88
+ width: number;
89
+ height: number;
90
+ };
91
+
92
+ /** 解析计算样式中的像素长度,非法值按 0 处理 */
93
+ const toPixel = (value: string) => Number.parseFloat(value) || 0;
94
+
95
+ /** 读取单元格内容盒矩形:扣除边框与内边距 */
96
+ const measureContentRect = (cell: HTMLElement): ContentRect => {
97
+ const rect = cell.getBoundingClientRect();
98
+ const style = getComputedStyle(cell);
99
+ const insetLeft = toPixel(style.borderLeftWidth) + toPixel(style.paddingLeft);
100
+ const insetTop = toPixel(style.borderTopWidth) + toPixel(style.paddingTop);
101
+ const insetRight =
102
+ toPixel(style.borderRightWidth) + toPixel(style.paddingRight);
103
+ const insetBottom =
104
+ toPixel(style.borderBottomWidth) + toPixel(style.paddingBottom);
105
+
106
+ return {
107
+ left: rect.left + insetLeft,
108
+ top: rect.top + insetTop,
109
+ width: Math.max(0, rect.width - insetLeft - insetRight),
110
+ height: Math.max(0, rect.height - insetTop - insetBottom),
111
+ };
112
+ };
113
+
114
+ /** 读取单元格文本行矩形并裁剪到内容盒,文本行不可用时回退内容盒 */
115
+ const measureTextRect = (
116
+ cell: HTMLElement,
117
+ content: ContentRect
118
+ ): ContentRect => {
119
+ if (
120
+ typeof document === 'undefined' ||
121
+ typeof document.createRange !== 'function'
122
+ ) {
123
+ return content;
124
+ }
125
+ const range = document.createRange();
126
+ range.selectNodeContents(cell);
127
+ const rect = range.getBoundingClientRect();
128
+ if (!rect.width || !rect.height) return content;
129
+ const left = Math.max(rect.left, content.left);
130
+ const top = Math.max(rect.top, content.top);
131
+ const right = Math.min(rect.right, content.left + content.width);
132
+ const bottom = Math.min(rect.bottom, content.top + content.height);
133
+ if (right <= left || bottom <= top) return content;
134
+ return {
135
+ left,
136
+ top,
137
+ width: right - left,
138
+ height: bottom - top,
139
+ };
140
+ };
141
+
71
142
  /**
72
143
  * 表格级共享省略 Tooltip
73
144
  *
74
145
  * @description
75
- * 在 ProTable 根节点上以事件委托识别带标记的 body 单元格,维护单个 Tooltip 的显隐、文本与固定定位锚点;
76
- * 仅在单元格文本横向溢出时触发;表格滚动、窗口尺寸变化、KeepAlive 停用时关闭,卸载时移除监听。
146
+ * 在 ProTable 根节点上以事件委托识别带标记的 body 单元格,维护单个 Tooltip 的显隐、文本与按单元格文本行矩形计算的固定定位锚点,文本行矩形不可用时回退单元格内容盒;
147
+ * 仅在单元格文本横向溢出时触发;鼠标移入浮层时保持展示,移出浮层后按离开延迟关闭;
148
+ * 表格滚动、窗口尺寸变化、KeepAlive 停用时关闭,卸载时移除监听。
77
149
  *
78
150
  * @param rootRef - ProTable 根节点引用
79
151
  * @param enabled - 是否启用共享 Tooltip;为 false 时不注册全局监听
80
152
  * @param delay - 悬停延迟(ms),0 表示立即展示
81
153
  * @param leaveDelay - 移出延迟(ms),0 表示立即关闭
154
+ * @param overlayElement - 当前 Tooltip 浮层节点引用
82
155
  * @returns Tooltip 状态与根节点事件处理器
83
156
  */
84
157
  export function useEllipsisTooltip(
85
158
  rootRef: Ref<HTMLElement | null | undefined>,
86
159
  enabled: Ref<boolean>,
87
160
  delay: Ref<number>,
88
- leaveDelay: Ref<number>
161
+ leaveDelay: Ref<number>,
162
+ overlayElement: Ref<HTMLElement | null | undefined>
89
163
  ): EllipsisTooltipState {
90
164
  const open = ref(false);
91
165
  const content = ref('');
@@ -94,14 +168,18 @@ export function useEllipsisTooltip(
94
168
  });
95
169
  let activeCell: HTMLElement | null = null;
96
170
  let pendingCell: HTMLElement | null = null;
171
+ let pendingContent = '';
172
+ let pendingRect: ContentRect | null = null;
97
173
  let openTimer: ReturnType<typeof setTimeout> | null = null;
98
174
  let closeTimer: ReturnType<typeof setTimeout> | null = null;
99
175
 
100
176
  const clearOpenTimer = () => {
177
+ pendingCell = null;
178
+ pendingContent = '';
179
+ pendingRect = null;
101
180
  if (openTimer === null) return;
102
181
  clearTimeout(openTimer);
103
182
  openTimer = null;
104
- pendingCell = null;
105
183
  };
106
184
 
107
185
  const clearCloseTimer = () => {
@@ -110,16 +188,82 @@ export function useEllipsisTooltip(
110
188
  closeTimer = null;
111
189
  };
112
190
 
191
+ const getEventRoot = (node: Node | null) => {
192
+ const element = node instanceof Element ? node : node?.parentElement;
193
+ return element?.closest?.(`[${PRO_TABLE_ROOT_ATTR}]`) ?? null;
194
+ };
195
+
113
196
  const close = () => {
114
197
  clearOpenTimer();
115
198
  clearCloseTimer();
116
199
  open.value = false;
117
200
  activeCell = null;
201
+ removeOverlayListeners();
118
202
  };
119
203
 
120
- const getEventRoot = (node: Node | null) => {
121
- const element = node instanceof Element ? node : node?.parentElement;
122
- return element?.closest?.(`[${PRO_TABLE_ROOT_ATTR}]`) ?? null;
204
+ const scheduleClose = () => {
205
+ clearCloseTimer();
206
+ const delayMs = Math.max(0, leaveDelay.value);
207
+ if (delayMs === 0) {
208
+ close();
209
+ return;
210
+ }
211
+ closeTimer = setTimeout(() => {
212
+ closeTimer = null;
213
+ close();
214
+ }, delayMs);
215
+ };
216
+
217
+ const isOverlayNode = (node: EventTarget | null) => {
218
+ const overlay = overlayElement.value;
219
+ return Boolean(overlay && node instanceof Node && overlay.contains(node));
220
+ };
221
+
222
+ const isForeignOverlayNode = (node: EventTarget | null) => {
223
+ if (!(node instanceof Node)) return false;
224
+ const own = overlayElement.value;
225
+ for (const overlay of activeOverlays) {
226
+ if (overlay !== own && overlay.contains(node)) return true;
227
+ }
228
+ return false;
229
+ };
230
+
231
+ const onOverlayMouseOver = (event: MouseEvent) => {
232
+ if (!isOverlayNode(event.target)) return;
233
+ clearCloseTimer();
234
+ };
235
+
236
+ const onOverlayMouseOut = (event: MouseEvent) => {
237
+ if (!isOverlayNode(event.target)) return;
238
+ const related = event.relatedTarget;
239
+ if (isOverlayNode(related)) return;
240
+ if (isForeignOverlayNode(related)) {
241
+ close();
242
+ return;
243
+ }
244
+ if (activeCell && related instanceof Node && activeCell.contains(related)) {
245
+ const relatedRoot = getEventRoot(related);
246
+ if (!relatedRoot || relatedRoot === rootRef.value) return;
247
+ close();
248
+ return;
249
+ }
250
+ scheduleClose();
251
+ };
252
+
253
+ let overlayListening = false;
254
+
255
+ const addOverlayListeners = () => {
256
+ if (overlayListening || typeof document === 'undefined') return;
257
+ overlayListening = true;
258
+ document.addEventListener('mouseover', onOverlayMouseOver);
259
+ document.addEventListener('mouseout', onOverlayMouseOut);
260
+ };
261
+
262
+ const removeOverlayListeners = () => {
263
+ if (!overlayListening) return;
264
+ overlayListening = false;
265
+ document.removeEventListener('mouseover', onOverlayMouseOver);
266
+ document.removeEventListener('mouseout', onOverlayMouseOut);
123
267
  };
124
268
 
125
269
  const getCellFromEvent = (event: MouseEvent) => {
@@ -140,24 +284,32 @@ export function useEllipsisTooltip(
140
284
  close();
141
285
  const measurement = measureCell(cell);
142
286
  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
- };
287
+ const contentRect = measureContentRect(cell);
288
+ if (!contentRect.width || !contentRect.height) return;
289
+ const rect = measureTextRect(cell, contentRect);
290
+ pendingContent = measurement.text;
291
+ pendingRect = rect;
153
292
  pendingCell = cell;
154
293
  openTimer = setTimeout(
155
294
  () => {
156
295
  openTimer = null;
157
296
  pendingCell = null;
158
- if (!cell.isConnected) return;
297
+ const anchorRect = pendingRect;
298
+ const nextContent = pendingContent;
299
+ pendingContent = '';
300
+ pendingRect = null;
301
+ if (!cell.isConnected || !anchorRect) return;
302
+ content.value = nextContent;
303
+ anchorStyle.value = {
304
+ ...ANCHOR_BASE_STYLE,
305
+ left: `${anchorRect.left}px`,
306
+ top: `${anchorRect.top}px`,
307
+ width: `${anchorRect.width}px`,
308
+ height: `${anchorRect.height}px`,
309
+ };
159
310
  activeCell = cell;
160
311
  open.value = true;
312
+ addOverlayListeners();
161
313
  },
162
314
  Math.max(0, delay.value)
163
315
  );
@@ -171,6 +323,11 @@ export function useEllipsisTooltip(
171
323
  if (!owner || !target || !owner.contains(target)) return;
172
324
 
173
325
  const related = event.relatedTarget as Node | null;
326
+ if (isOverlayNode(related)) return;
327
+ if (isForeignOverlayNode(related)) {
328
+ close();
329
+ return;
330
+ }
174
331
  if (related && owner.contains(related)) {
175
332
  const relatedRoot = getEventRoot(related);
176
333
  if (!relatedRoot || relatedRoot === rootRef.value) return;
@@ -183,16 +340,7 @@ export function useEllipsisTooltip(
183
340
  return;
184
341
  }
185
342
 
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);
343
+ scheduleClose();
196
344
  };
197
345
 
198
346
  const onViewportChange = () => close();
@@ -229,6 +377,7 @@ export function useEllipsisTooltip(
229
377
 
230
378
  onBeforeUnmount(() => {
231
379
  removeViewportListeners();
380
+ removeOverlayListeners();
232
381
  clearOpenTimer();
233
382
  clearCloseTimer();
234
383
  });