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;