修复提示框的弹出位置移动的问题
This commit is contained in:
@@ -147,7 +147,8 @@ interface ReviewPointsListProps {
|
||||
let activeTooltip = {
|
||||
show: false, // 控制提示框是否显示
|
||||
content: null as React.ReactNode, // 提示框内容(React节点)
|
||||
position: { top: 0, left: 0 } // 提示框在屏幕上的位置
|
||||
position: { top: 0, left: 0 }, // 提示框在屏幕上的位置
|
||||
ready: false // 新增:控制是否已准备好显示
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -186,7 +187,10 @@ function TooltipPortal() {
|
||||
style={{
|
||||
top: `${tooltip.position.top}px`,
|
||||
left: `${tooltip.position.left}px`,
|
||||
transform: 'translate(-100%, -50%)' // 调整位置,使提示框在指针左侧居中显示
|
||||
transform: 'translate(-100%, -50%)', // 调整位置,使提示框在指针左侧居中显示
|
||||
opacity: tooltip.ready ? 1 : 0, // 根据ready状态控制透明度
|
||||
visibility: tooltip.ready ? 'visible' : 'hidden', // 使用visibility确保在位置计算时元素存在但不可见
|
||||
transition: 'opacity 0.15s ease-out' // 添加平滑过渡效果
|
||||
}}
|
||||
>
|
||||
{tooltip.content}
|
||||
@@ -203,22 +207,66 @@ function TooltipPortal() {
|
||||
* @param position 显示位置坐标
|
||||
*/
|
||||
function showTooltip(content: React.ReactNode, position: { top: number; left: number }): void {
|
||||
// 更新全局状态对象
|
||||
// 先设置内容和位置,但不立即显示
|
||||
activeTooltip = {
|
||||
show: true,
|
||||
content,
|
||||
position
|
||||
position,
|
||||
ready: false // 初始设为未准备好
|
||||
};
|
||||
// 触发自定义事件,通知TooltipPortal组件更新状态
|
||||
|
||||
// 触发事件,让TooltipPortal渲染tooltip(但不可见)
|
||||
window.dispatchEvent(new Event('tooltip-update'));
|
||||
|
||||
// 使用RAF确保tooltip已渲染到DOM后再计算最终位置
|
||||
requestAnimationFrame(() => {
|
||||
// 查找刚创建的tooltip元素
|
||||
const tooltipElement = document.querySelector('.fixed.bg-white.shadow-lg.rounded-md') as HTMLElement;
|
||||
|
||||
if (tooltipElement) {
|
||||
// 获取tooltip的实际尺寸
|
||||
const tooltipRect = tooltipElement.getBoundingClientRect();
|
||||
|
||||
// 重新计算位置,确保tooltip不会超出视口
|
||||
let adjustedTop = position.top;
|
||||
let adjustedLeft = position.left;
|
||||
|
||||
// 检查是否超出右边界
|
||||
if (adjustedLeft - tooltipRect.width < 0) {
|
||||
adjustedLeft = tooltipRect.width + 10; // 留一些边距
|
||||
}
|
||||
|
||||
// 检查是否超出上边界
|
||||
if (adjustedTop - tooltipRect.height / 2 < 0) {
|
||||
adjustedTop = tooltipRect.height / 2 + 10;
|
||||
}
|
||||
|
||||
// 检查是否超出下边界
|
||||
if (adjustedTop + tooltipRect.height / 2 > window.innerHeight) {
|
||||
adjustedTop = window.innerHeight - tooltipRect.height / 2 - 10;
|
||||
}
|
||||
|
||||
// 更新位置并设为准备好显示
|
||||
activeTooltip.position = { top: adjustedTop, left: adjustedLeft };
|
||||
activeTooltip.ready = true;
|
||||
|
||||
// 再次触发事件更新显示状态
|
||||
window.dispatchEvent(new Event('tooltip-update'));
|
||||
} else {
|
||||
// 如果找不到tooltip元素,直接显示
|
||||
activeTooltip.ready = true;
|
||||
window.dispatchEvent(new Event('tooltip-update'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏提示框的辅助函数
|
||||
*/
|
||||
function hideTooltip(): void {
|
||||
// 设置为不显示状态
|
||||
// 设置为不显示状态并重置ready状态
|
||||
activeTooltip.show = false;
|
||||
activeTooltip.ready = false;
|
||||
// 触发自定义事件,通知TooltipPortal组件更新状态
|
||||
window.dispatchEvent(new Event('tooltip-update'));
|
||||
}
|
||||
@@ -232,6 +280,7 @@ function hideTooltip(): void {
|
||||
*/
|
||||
const ReactTableTooltip = ({ content }: { content: string }) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [renderedContent, setRenderedContent] = useState<React.ReactNode>(null);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isTableLike = content.includes('\t') && content.includes('\n');
|
||||
@@ -245,7 +294,14 @@ const ReactTableTooltip = ({ content }: { content: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(checkTextOverflow, 0);
|
||||
// 预渲染内容并缓存
|
||||
if (isTableLike) {
|
||||
setRenderedContent(renderReactTable(content));
|
||||
} else {
|
||||
setRenderedContent(content);
|
||||
}
|
||||
|
||||
requestAnimationFrame(checkTextOverflow);
|
||||
window.addEventListener('resize', checkTextOverflow);
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkTextOverflow);
|
||||
@@ -310,14 +366,14 @@ const ReactTableTooltip = ({ content }: { content: string }) => {
|
||||
<div className="text-xs p-1 rounded cursor-text w-full text-left">
|
||||
{showTooltip ? (
|
||||
<Tooltip
|
||||
content={isTableLike ? renderReactTable(content) : content}
|
||||
content={renderedContent}
|
||||
placement="top"
|
||||
theme="light"
|
||||
trigger="hover"
|
||||
showArrow={true}
|
||||
className="tooltip-custom-offset"
|
||||
// fixedPlacement={true}
|
||||
// scrollable={true}
|
||||
scrollable={true}
|
||||
maxHeight={400}
|
||||
>
|
||||
<div className="text-gray-800 break-all overflow-hidden line-clamp-2" ref={textRef}>
|
||||
@@ -1561,8 +1617,8 @@ export function ReviewPointsList({
|
||||
fieldElements.push(
|
||||
<div key="message" className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||||
<div className="flex flex-row items-center">
|
||||
<i className="ri-robot-2-line text-blue-500 mr-2 "></i>
|
||||
<p className="text-xs text-gray-600 select-text">{messageContent}</p>
|
||||
<i className="ri-robot-2-line text-blue-500 mr-2"></i>
|
||||
<p className="text-xs text-gray-600 select-text mb-0">{messageContent}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1980,10 +2036,10 @@ export function ReviewPointsList({
|
||||
|
||||
{/* 建议内容显示区域 */}
|
||||
{reviewPoint.suggestion && (
|
||||
<div className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||||
<div className="flex items-start">
|
||||
<div className="p-2 bg-blue-50 rounded border border-blue-200 mb-3 text-xs select-text">
|
||||
<div className="flex items-center flex-row">
|
||||
<i className="ri-information-line text-blue-500 mr-2 mt-0.5"></i>
|
||||
<p className="text-xs text-gray-600 select-text text-left">{reviewPoint.suggestion}</p>
|
||||
<p className="text-xs text-gray-600 select-text text-left mb-0">{reviewPoint.suggestion}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -60,6 +60,9 @@ export function Tooltip({
|
||||
// 获取实际显示状态
|
||||
const isVisible = isControlled ? controlledVisible : visible;
|
||||
|
||||
// 添加一个状态来控制实际渲染,解决位置跳跃问题
|
||||
const [readyToShow, setReadyToShow] = useState(false);
|
||||
|
||||
// 引用DOM元素
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
@@ -73,11 +76,17 @@ export function Tooltip({
|
||||
if (!isControlled) {
|
||||
setVisible(newVisible);
|
||||
}
|
||||
onVisibleChange?.(newVisible);
|
||||
|
||||
// 当显示状态变为false时,重置actualPlacement为初始placement
|
||||
// 当显示状态变为false时,立即隐藏
|
||||
if (!newVisible) {
|
||||
setReadyToShow(false);
|
||||
onVisibleChange?.(newVisible);
|
||||
// 当显示状态变为false时,重置actualPlacement为初始placement
|
||||
setActualPlacement(placement);
|
||||
} else {
|
||||
// 当显示状态变为true时,先延迟设置readyToShow
|
||||
// 这样允许位置预计算完成后再显示tooltip
|
||||
onVisibleChange?.(newVisible);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -97,7 +106,6 @@ export function Tooltip({
|
||||
|
||||
// 获取触发元素位置
|
||||
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||
|
||||
// 设置最大宽度
|
||||
tooltipRef.current.style.maxWidth = typeof maxWidth === 'number'
|
||||
@@ -115,6 +123,16 @@ export function Tooltip({
|
||||
tooltipRef.current.style.overflow = 'visible';
|
||||
}
|
||||
|
||||
// 强制重新计算布局,确保maxHeight生效后再获取尺寸
|
||||
tooltipRef.current.style.visibility = 'hidden';
|
||||
tooltipRef.current.style.display = 'block';
|
||||
|
||||
// 触发重排,确保maxHeight限制已应用
|
||||
void tooltipRef.current.offsetHeight;
|
||||
|
||||
// 获取应用maxHeight后的实际tooltip尺寸
|
||||
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||
|
||||
// 计算各个方向的位置
|
||||
let top = 0, left = 0;
|
||||
let arrowTop = 0, arrowLeft = 0;
|
||||
@@ -311,6 +329,17 @@ export function Tooltip({
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复可见性(位置计算完成后)
|
||||
tooltipRef.current.style.visibility = 'visible';
|
||||
|
||||
// 如果位置已更新且还没有显示,现在可以显示了
|
||||
if (!readyToShow) {
|
||||
// 使用requestAnimationFrame确保DOM更新后再显示
|
||||
requestAnimationFrame(() => {
|
||||
setReadyToShow(true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 当组件首次挂载或placement改变时,重置actualPlacement
|
||||
@@ -318,12 +347,27 @@ export function Tooltip({
|
||||
setActualPlacement(placement);
|
||||
}, [placement]);
|
||||
|
||||
// 计算提示框位置
|
||||
// 当isVisible状态变化时,处理初始化和清理工作
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
if (!isVisible) {
|
||||
// 当tooltip隐藏时,重置readyToShow状态
|
||||
setReadyToShow(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化位置
|
||||
updateTooltipPosition();
|
||||
// 创建一个隐藏的tooltip用于预计算位置
|
||||
// 使用一个隐藏的div来渲染tooltip,并获取其尺寸
|
||||
const preCalculatePosition = () => {
|
||||
// 初始时先不显示,让updateTooltipPosition完成位置计算
|
||||
setReadyToShow(false);
|
||||
|
||||
// 初始化位置计算 - 添加短暂延迟确保内容已渲染
|
||||
setTimeout(() => {
|
||||
updateTooltipPosition();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
preCalculatePosition();
|
||||
|
||||
// 添加滚动和调整大小事件监听器
|
||||
window.addEventListener('scroll', updateTooltipPosition, true);
|
||||
@@ -338,7 +382,7 @@ export function Tooltip({
|
||||
window.removeEventListener('resize', updateTooltipPosition);
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [isVisible, placement, maxWidth, showArrow, fixedPlacement, actualPlacement]);
|
||||
}, [isVisible, placement, maxWidth, showArrow, fixedPlacement]);
|
||||
|
||||
// 处理点击外部关闭
|
||||
useEffect(() => {
|
||||
@@ -389,18 +433,23 @@ export function Tooltip({
|
||||
`tooltip-${actualPlacement}`, // 使用actualPlacement而不是placement
|
||||
`tooltip-${theme}`,
|
||||
rich ? 'tooltip-rich' : '',
|
||||
isVisible ? 'tooltip-visible' : '',
|
||||
readyToShow ? 'tooltip-visible' : 'tooltip-hidden', // 使用readyToShow控制可见性
|
||||
fixedPlacement ? 'tooltip-fixed-placement' : '',
|
||||
className
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
// 使用Portal渲染提示框
|
||||
// 使用Portal渲染提示框,但根据readyToShow来控制可见性样式
|
||||
const tooltipPortal = isVisible && typeof document !== 'undefined'
|
||||
? createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
className={tooltipClassNames}
|
||||
style={{ maxWidth }}
|
||||
style={{
|
||||
maxWidth,
|
||||
opacity: readyToShow ? 1 : 0, // 根据readyToShow控制透明度
|
||||
visibility: readyToShow ? 'visible' : 'hidden', // 使用visibility确保在位置计算时元素存在但不可见
|
||||
transition: 'opacity 0.1s ease-out' // 添加平滑过渡效果
|
||||
}}
|
||||
>
|
||||
{renderTooltipContent()}
|
||||
{showArrow && <div className="tooltip-arrow"></div>}
|
||||
|
||||
Reference in New Issue
Block a user