fix(frontend): reposition card hover popover to card instead of cursor

Change the knowledge card hover popover from following the mouse to being
anchored to the card element with smart boundary detection. Try positioning
in order: right, left, bottom, and top of the card, automatically avoiding
viewport edges. Also add CSS hints (will-change, backface-visibility,
translateZ) to prevent popover content jitter.
This commit is contained in:
Binks Riverton
2026-06-26 22:26:27 +08:00
committed by lyingbug
parent 8285188501
commit 7d8a80ae8d
+106 -12
View File
@@ -1226,35 +1226,121 @@ const openSourceDoc = (knowledgeId: string) => {
getCardDetails({ id: knowledgeId });
};
// 悬停知识卡片时跟随鼠标显示详情气泡
// 悬停知识卡片时显示详情气泡(基于卡片位置定位)
const hoveredCardItem = ref<KnowledgeCard | null>(null);
const cardPopoverPos = ref({ x: 0, y: 0 });
const CARD_POPOVER_OFFSET = 16;
const CARD_POPOVER_OFFSET = 12;
const CARD_POPOVER_ESTIMATED_WIDTH = 360;
const CARD_POPOVER_ESTIMATED_HEIGHT = 300;
const cardHoverShowDelay = 300;
let cardHoverTimer: ReturnType<typeof setTimeout> | null = null;
let cardPopoverElement: HTMLElement | null = null;
// 根据卡片位置计算气泡位置(优先右侧,自动避开边界)
const calculatePopoverPositionFromCard = (cardElement: HTMLElement): { x: number; y: number } => {
const cardRect = cardElement.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 获取实际气泡尺寸
let popoverWidth = CARD_POPOVER_ESTIMATED_WIDTH;
let popoverHeight = CARD_POPOVER_ESTIMATED_HEIGHT;
if (cardPopoverElement) {
const rect = cardPopoverElement.getBoundingClientRect();
if (rect.width > 0) popoverWidth = rect.width;
if (rect.height > 0) popoverHeight = rect.height;
}
let x = 0;
let y = 0;
// 策略1:优先尝试放在卡片右侧
const rightX = cardRect.right + CARD_POPOVER_OFFSET;
if (rightX + popoverWidth <= viewportWidth - 10) {
x = rightX;
y = cardRect.top;
// 确保不超出底部
if (y + popoverHeight > viewportHeight - 10) {
y = viewportHeight - popoverHeight - 10;
}
// 确保不超出顶部
y = Math.max(10, y);
return { x, y };
}
// 策略2:尝试放在卡片左侧
const leftX = cardRect.left - popoverWidth - CARD_POPOVER_OFFSET;
if (leftX >= 10) {
x = leftX;
y = cardRect.top;
// 确保不超出底部
if (y + popoverHeight > viewportHeight - 10) {
y = viewportHeight - popoverHeight - 10;
}
// 确保不超出顶部
y = Math.max(10, y);
return { x, y };
}
// 策略3:尝试放在卡片下方
const bottomY = cardRect.bottom + CARD_POPOVER_OFFSET;
if (bottomY + popoverHeight <= viewportHeight - 10) {
y = bottomY;
x = cardRect.left;
// 确保不超出右边界
if (x + popoverWidth > viewportWidth - 10) {
x = viewportWidth - popoverWidth - 10;
}
// 确保不超出左边界
x = Math.max(10, x);
return { x, y };
}
// 策略4:放在卡片上方
const topY = cardRect.top - popoverHeight - CARD_POPOVER_OFFSET;
y = Math.max(10, topY);
x = cardRect.left;
// 确保不超出右边界
if (x + popoverWidth > viewportWidth - 10) {
x = viewportWidth - popoverWidth - 10;
}
// 确保不超出左边界
x = Math.max(10, x);
return { x, y };
};
const onCardMouseEnter = (ev: MouseEvent, item: KnowledgeCard) => {
if (cardHoverTimer) {
clearTimeout(cardHoverTimer);
cardHoverTimer = null;
}
const cardElement = (ev.currentTarget as HTMLElement);
cardHoverTimer = setTimeout(() => {
cardHoverTimer = null;
hoveredCardItem.value = item;
cardPopoverPos.value = {
x: ev.clientX + CARD_POPOVER_OFFSET,
y: ev.clientY + CARD_POPOVER_OFFSET,
};
// 基于卡片位置计算气泡位置
const pos = calculatePopoverPositionFromCard(cardElement);
cardPopoverPos.value = pos;
// 获取实际元素后精确计算
nextTick(() => {
cardPopoverElement = document.querySelector('.knowledge-card-hover-popover') as HTMLElement;
if (cardPopoverElement) {
const refinedPos = calculatePopoverPositionFromCard(cardElement);
cardPopoverPos.value = refinedPos;
}
});
}, cardHoverShowDelay);
};
// 鼠标在卡片上移动时不更新气泡位置
const onCardMouseMove = (ev: MouseEvent) => {
if (hoveredCardItem.value) {
cardPopoverPos.value = {
x: ev.clientX + CARD_POPOVER_OFFSET,
y: ev.clientY + CARD_POPOVER_OFFSET,
};
}
// 保持气泡固定在卡片旁边
};
const onCardMouseLeave = () => {
@@ -1263,6 +1349,7 @@ const onCardMouseLeave = () => {
cardHoverTimer = null;
}
hoveredCardItem.value = null;
cardPopoverElement = null;
};
const closeCardMoreMenu = (index: number) => {
@@ -4145,6 +4232,13 @@ async function createNewSession(value: string): Promise<void> {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
font-family: var(--app-font-family);
transition: opacity 0.15s ease;
will-change: transform;
/* 防止气泡内容抖动 */
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform: translateZ(0);
-webkit-transform: translateZ(0);
.card-popover-title {
font-size: 14px;