From 820baa5b2297af740bb2c326689a00523b43bf45 Mon Sep 17 00:00:00 2001
From: yorn <1057707203@qq.com>
Date: Sun, 1 Jun 2025 23:53:07 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96tooltip=E7=BB=84=E4=BB=B6?=
=?UTF-8?q?=E6=8F=90=E7=A4=BA=E6=A1=86=E7=9A=84=E5=BC=B9=E5=87=BA=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E6=98=BE=E7=A4=BA=EF=BC=8C=E8=A7=A3=E5=86=B3=E9=BC=A0?=
=?UTF-8?q?=E6=A0=87=E4=BB=8E=E7=9B=AE=E6=A0=87=E5=85=83=E7=B4=A0=E7=A7=BB?=
=?UTF-8?q?=E5=8A=A8=E5=88=B0=E6=8F=90=E7=A4=BA=E6=A1=86=E7=9A=84=E8=BF=87?=
=?UTF-8?q?=E7=A8=8B=E4=B8=AD=E6=8F=90=E7=A4=BA=E6=A1=86=E6=B6=88=E5=A4=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/components/reviews/ReviewPointsList.tsx | 4 +
app/components/ui/Tooltip.tsx | 158 ++++++++++++++++----
app/styles/components/TooltipStyles.css | 48 +++---
3 files changed, 162 insertions(+), 48 deletions(-)
diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx
index c0b6116..d809334 100644
--- a/app/components/reviews/ReviewPointsList.tsx
+++ b/app/components/reviews/ReviewPointsList.tsx
@@ -320,6 +320,7 @@ const ReactTableTooltip = ({ content }: { content: string }) => {
trigger="hover"
showArrow={true}
className="tooltip-custom-offset"
+ // fixedPlacement={true}
>
{content}
@@ -619,6 +620,7 @@ export function ReviewPointsList({
trigger="hover"
showArrow={true}
className="tooltip-custom-offset tooltip-top"
+ fixedPlacement={true}
>
通过
@@ -642,6 +644,7 @@ export function ReviewPointsList({
trigger="hover"
showArrow={true}
className="tooltip-custom-offset tooltip-top"
+ fixedPlacement={true}
>
警告
@@ -661,6 +664,7 @@ export function ReviewPointsList({
trigger="hover"
showArrow={true}
className="tooltip-custom-offset tooltip-top"
+ fixedPlacement={true}
>
不通过
diff --git a/app/components/ui/Tooltip.tsx b/app/components/ui/Tooltip.tsx
index 9ee9877..758ca40 100644
--- a/app/components/ui/Tooltip.tsx
+++ b/app/components/ui/Tooltip.tsx
@@ -23,6 +23,7 @@ export interface TooltipProps {
maxWidth?: number | string; // 最大宽度
className?: string; // 自定义类名
onVisibleChange?: (visible: boolean) => void; // 显示状态变化回调
+ fixedPlacement?: boolean; // 是否固定显示位置,不自动切换
}
/**
@@ -43,7 +44,8 @@ export function Tooltip({
showArrow = true,
maxWidth = 320,
className = '',
- onVisibleChange
+ onVisibleChange,
+ fixedPlacement = false // 默认不固定位置
}: TooltipProps) {
// 使用内部状态管理提示框显示状态(非受控模式)
const [visible, setVisible] = useState(false);
@@ -58,12 +60,20 @@ export function Tooltip({
const triggerRef = useRef(null);
const tooltipRef = useRef(null);
+ // 保存当前实际显示位置
+ const [actualPlacement, setActualPlacement] = useState(placement);
+
// 处理显示状态变化
const handleVisibleChange = (newVisible: boolean) => {
if (!isControlled) {
setVisible(newVisible);
}
onVisibleChange?.(newVisible);
+
+ // 当显示状态变为false时,重置actualPlacement为初始placement
+ if (!newVisible) {
+ setActualPlacement(placement);
+ }
};
// 触发器事件处理
@@ -95,7 +105,10 @@ export function Tooltip({
const arrowSize = 8; // 箭头大小
const gap = 10; // 提示框与触发元素的间距
- switch (placement) {
+ // 使用actualPlacement而不是placement来计算位置
+ let currentPlacement = actualPlacement;
+
+ switch (currentPlacement) {
case 'top':
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
top = triggerRect.top - tooltipRect.height - arrowSize;
@@ -104,7 +117,7 @@ export function Tooltip({
break;
case 'bottom':
left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
- top = triggerRect.bottom + arrowSize;
+ top = triggerRect.bottom + arrowSize - 8;
arrowLeft = tooltipRect.width / 2 - arrowSize;
arrowTop = -arrowSize;
break;
@@ -135,27 +148,105 @@ export function Tooltip({
arrowLeft = Math.min(triggerRect.left + (triggerRect.width / 2) - left - arrowSize, tooltipRect.width - arrowSize * 2);
}
- if (top < 0) {
- if (placement === 'top') {
- // 如果上方放不下,切换到下方
- top = triggerRect.bottom + arrowSize;
- arrowTop = -arrowSize;
- tooltipRef.current.classList.remove('tooltip-top');
- tooltipRef.current.classList.add('tooltip-bottom');
- } else {
- top = gap;
- arrowTop = Math.max(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, arrowSize * 2);
+ // 如果fixedPlacement为true,则强制使用指定的位置,不自动切换
+ if (!fixedPlacement) {
+ // 自动调整位置逻辑
+ if (top < 0) {
+ if (currentPlacement === 'top') {
+ // 如果上方放不下,切换到下方
+ top = triggerRect.bottom + arrowSize;
+ arrowTop = -arrowSize;
+
+ // 更新实际位置为bottom
+ setActualPlacement('bottom');
+ currentPlacement = 'bottom';
+
+ // 更新CSS类
+ tooltipRef.current.classList.remove('tooltip-top');
+ tooltipRef.current.classList.add('tooltip-bottom');
+ } else {
+ top = gap;
+ arrowTop = Math.max(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, arrowSize * 2);
+ }
+ } else if (top + tooltipRect.height > viewportHeight) {
+ if (currentPlacement === 'bottom') {
+ // 如果下方放不下,切换到上方
+ top = triggerRect.top - tooltipRect.height - arrowSize;
+ arrowTop = tooltipRect.height;
+
+ // 更新实际位置为top
+ setActualPlacement('top');
+ currentPlacement = 'top';
+
+ // 更新CSS类
+ tooltipRef.current.classList.remove('tooltip-bottom');
+ tooltipRef.current.classList.add('tooltip-top');
+ } else {
+ top = viewportHeight - tooltipRect.height - gap;
+ arrowTop = Math.min(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, tooltipRect.height - arrowSize * 2);
+ }
}
- } else if (top + tooltipRect.height > viewportHeight) {
- if (placement === 'bottom') {
- // 如果下方放不下,切换到上方
- top = triggerRect.top - tooltipRect.height - arrowSize;
- arrowTop = tooltipRect.height;
- tooltipRef.current.classList.remove('tooltip-bottom');
- tooltipRef.current.classList.add('tooltip-top');
- } else {
- top = viewportHeight - tooltipRect.height - gap;
- arrowTop = Math.min(triggerRect.top + (triggerRect.height / 2) - top - arrowSize, tooltipRect.height - arrowSize * 2);
+
+ // 处理左右方向的切换
+ if (left < 0 && currentPlacement === 'left') {
+ // 如果左边放不下,切换到右边
+ left = triggerRect.right + arrowSize;
+ arrowLeft = -arrowSize;
+
+ // 更新实际位置为right
+ setActualPlacement('right');
+ currentPlacement = 'right';
+
+ // 更新CSS类
+ tooltipRef.current.classList.remove('tooltip-left');
+ tooltipRef.current.classList.add('tooltip-right');
+ } else if (left + tooltipRect.width > viewportWidth && currentPlacement === 'right') {
+ // 如果右边放不下,切换到左边
+ left = triggerRect.left - tooltipRect.width - arrowSize;
+ arrowLeft = tooltipRect.width;
+
+ // 更新实际位置为left
+ setActualPlacement('left');
+ currentPlacement = 'left';
+
+ // 更新CSS类
+ tooltipRef.current.classList.remove('tooltip-right');
+ tooltipRef.current.classList.add('tooltip-left');
+ }
+ } else {
+ // 固定位置,但仍然需要确保在视口内可见
+ if (currentPlacement === 'top') {
+ // 如果提示框超出了顶部,增加偏移
+ if (top < 0) {
+ const originalTop = top;
+ top = gap;
+ // 调整箭头位置,使其指向触发元素
+ arrowTop = arrowTop + (originalTop - top);
+ }
+ } else if (currentPlacement === 'bottom') {
+ // 如果提示框超出了底部,减少偏移
+ if (top + tooltipRect.height > viewportHeight) {
+ const originalTop = top;
+ top = viewportHeight - tooltipRect.height - gap;
+ // 调整箭头位置,使其指向触发元素
+ arrowTop = arrowTop - (originalTop - top);
+ }
+ } else if (currentPlacement === 'left') {
+ // 如果提示框超出了左侧,增加偏移
+ if (left < 0) {
+ const originalLeft = left;
+ left = gap;
+ // 调整箭头位置,使其指向触发元素
+ arrowLeft = arrowLeft + (originalLeft - left);
+ }
+ } else if (currentPlacement === 'right') {
+ // 如果提示框超出了右侧,减少偏移
+ if (left + tooltipRect.width > viewportWidth) {
+ const originalLeft = left;
+ left = viewportWidth - tooltipRect.width - gap;
+ // 调整箭头位置,使其指向触发元素
+ arrowLeft = arrowLeft - (originalLeft - left);
+ }
}
}
@@ -168,35 +259,49 @@ export function Tooltip({
// 定位箭头
const arrow = tooltipRef.current.querySelector('.tooltip-arrow') as HTMLElement;
if (arrow && showArrow) {
- switch (placement) {
+ // 使用当前实际的位置来定位箭头
+ switch (currentPlacement) {
case 'top':
arrow.style.bottom = `-${arrowSize}px`;
arrow.style.left = `${arrowLeft}px`;
arrow.style.top = 'auto';
arrow.style.right = 'auto';
+ // 设置箭头指向下方的样式
+ arrow.className = 'tooltip-arrow tooltip-arrow-bottom';
break;
case 'bottom':
arrow.style.top = `-${arrowSize}px`;
arrow.style.left = `${arrowLeft}px`;
arrow.style.bottom = 'auto';
arrow.style.right = 'auto';
+ // 设置箭头指向上方的样式
+ arrow.className = 'tooltip-arrow tooltip-arrow-top';
break;
case 'left':
arrow.style.right = `-${arrowSize}px`;
arrow.style.top = `${arrowTop}px`;
arrow.style.bottom = 'auto';
arrow.style.left = 'auto';
+ // 设置箭头指向右方的样式
+ arrow.className = 'tooltip-arrow tooltip-arrow-right';
break;
case 'right':
arrow.style.left = `-${arrowSize}px`;
arrow.style.top = `${arrowTop}px`;
arrow.style.bottom = 'auto';
arrow.style.right = 'auto';
+ // 设置箭头指向左方的样式
+ arrow.className = 'tooltip-arrow tooltip-arrow-left';
break;
}
}
};
+ // 当组件首次挂载或placement改变时,重置actualPlacement
+ useEffect(() => {
+ setActualPlacement(placement);
+ }, [placement]);
+
// 计算提示框位置
useEffect(() => {
if (!isVisible) return;
@@ -217,7 +322,7 @@ export function Tooltip({
window.removeEventListener('resize', updateTooltipPosition);
clearInterval(intervalId);
};
- }, [isVisible, placement, maxWidth, showArrow]);
+ }, [isVisible, placement, maxWidth, showArrow, fixedPlacement, actualPlacement]);
// 处理点击外部关闭
useEffect(() => {
@@ -262,10 +367,11 @@ export function Tooltip({
// 生成提示框类名
const tooltipClassNames = [
'tooltip-container',
- `tooltip-${placement}`,
+ `tooltip-${actualPlacement}`, // 使用actualPlacement而不是placement
`tooltip-${theme}`,
rich ? 'tooltip-rich' : '',
isVisible ? 'tooltip-visible' : '',
+ fixedPlacement ? 'tooltip-fixed-placement' : '',
className
].filter(Boolean).join(' ');
diff --git a/app/styles/components/TooltipStyles.css b/app/styles/components/TooltipStyles.css
index 7b41fda..61e708a 100644
--- a/app/styles/components/TooltipStyles.css
+++ b/app/styles/components/TooltipStyles.css
@@ -103,27 +103,48 @@
border-color: #3b82f6;
}
-/* Tooltip 位置基本样式 */
-.tooltip-top .tooltip-arrow {
+/* Tooltip 位置基本样式 - 根据箭头方向而不是容器位置 */
+/* 箭头朝下 - 用于顶部提示框 */
+.tooltip-arrow-bottom {
border-width: 8px 8px 0 8px;
border-color: inherit transparent transparent transparent;
}
-.tooltip-bottom .tooltip-arrow {
+/* 箭头朝上 - 用于底部提示框 */
+.tooltip-arrow-top {
border-width: 0 8px 8px 8px;
border-color: transparent transparent inherit transparent;
}
-.tooltip-left .tooltip-arrow {
+/* 箭头朝右 - 用于左侧提示框 */
+.tooltip-arrow-right {
border-width: 8px 0 8px 8px;
border-color: transparent transparent transparent inherit;
}
-.tooltip-right .tooltip-arrow {
+/* 箭头朝左 - 用于右侧提示框 */
+.tooltip-arrow-left {
border-width: 8px 8px 8px 0;
border-color: transparent inherit transparent transparent;
}
+/* 浅色主题下的箭头样式修正 */
+.tooltip-light .tooltip-arrow-bottom {
+ border-color: #ffffff transparent transparent transparent;
+}
+
+.tooltip-light .tooltip-arrow-top {
+ border-color: transparent transparent #ffffff transparent;
+}
+
+.tooltip-light .tooltip-arrow-right {
+ border-color: transparent transparent transparent #ffffff;
+}
+
+.tooltip-light .tooltip-arrow-left {
+ border-color: transparent #ffffff transparent transparent;
+}
+
/* 富文本提示框样式 */
.tooltip-rich .tooltip-content {
padding: 0;
@@ -168,23 +189,6 @@
border-top-color: #e2e8f0;
}
-/* 浅色主题下的箭头样式修正 */
-.tooltip-light.tooltip-top .tooltip-arrow {
- border-color: #ffffff transparent transparent transparent;
-}
-
-.tooltip-light.tooltip-bottom .tooltip-arrow {
- border-color: transparent transparent #ffffff transparent;
-}
-
-.tooltip-light.tooltip-left .tooltip-arrow {
- border-color: transparent transparent transparent #ffffff;
-}
-
-.tooltip-light.tooltip-right .tooltip-arrow {
- border-color: transparent #ffffff transparent transparent;
-}
-
/* 表格样式 */
.tooltip-content table {
border-collapse: collapse;