修复提示框的弹出位置移动的问题

This commit is contained in:
2025-06-09 19:06:50 +08:00
parent 880e68d92c
commit 534e1ba153
8 changed files with 635 additions and 32 deletions
+6 -4
View File
@@ -1,5 +1,6 @@
import axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios';
import { mockData, type MockApiResponse } from './mock';
import { API_BASE_URL, DOCUMENT_URL } from '../config/api-config';
/**
* API响应类型
@@ -13,14 +14,15 @@ export type ApiResponse<T> = {
export type QueryParams = Record<string, string | number | boolean | undefined>;
// 获取 API 基础 URL
// 获取 API 基础 URL (从配置文件导入)
// const API_BASE_URL = 'http://172.16.0.58:8008';
const API_BASE_URL = 'http://nas.7bm.co:3000';
// const API_BASE_URL = 'http://nas.7bm.co:3000';
// const API_BASE_URL = 'http://172.18.0.100:3000';
// const API_BASE_URL = 'http://172.16.0.119:9000/admin';
// 文档URL前缀
export const DOCUMENT_URL = 'http://nas.7bm.co:9000/docauditai/';
// 文档URL前缀 (从配置文件导入)
// export const DOCUMENT_URL = 'http://nas.7bm.co:9000/docauditai/';
export { DOCUMENT_URL };
// 是否使用模拟数据(开发环境使用)
const USE_MOCK_DATA = false; // 设置为true使用模拟数据,避免API连接问题
+3 -2
View File
@@ -1,5 +1,6 @@
import { postgrestGet, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
import { UPLOAD_URL } from '../../config/api-config';
// import { API_BASE_URL } from '../client';
/**
@@ -153,13 +154,13 @@ export async function uploadDocumentToServer(
formData.append('upload_info', JSON.stringify(uploadInfo));
// console.log('【调试】FormData准备完成:', JSON.stringify(uploadInfo));
// console.log('【调试】准备发送请求到服务器:', 'http://172.16.0.58:8008/admin/documents/upload');
// console.log('【调试】准备发送请求到服务器:', UPLOAD_URL);
// 发送请求
// const response = await fetch(`${API_BASE_URL}/admin/documents/upload`, {
try {
// console.log('【调试】开始fetch请求...');
const response = await fetch('http://172.16.0.58:8008/admin/documents/upload', {
const response = await fetch(UPLOAD_URL, {
// const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', {
// const response = await fetch('http://172.16.0.119:8000/admin/documents/upload', {
method: 'POST',
+70 -14
View File
@@ -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 -11
View File
@@ -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>}
+104
View File
@@ -0,0 +1,104 @@
/**
* API配置文件
* 统一管理所有API地址,方便部署时修改
* 支持环境变量覆盖配置
*/
// 环境配置类型
interface ApiConfig {
// 主API基础URL
baseUrl: string;
// 文档服务URL
documentUrl: string;
// 文档上传API URL
uploadUrl: string;
// PostgREST URL (如果使用)
postgrestUrl?: string;
}
// 不同环境的默认配置
const configs: Record<string, ApiConfig> = {
// 开发环境
development: {
baseUrl: 'http://nas.7bm.co:3000',
documentUrl: 'http://nas.7bm.co:9000/docauditai/',
uploadUrl: 'http://172.16.0.58:8008/admin/documents/upload',
},
// 测试环境
testing: {
baseUrl: 'http://172.18.0.100:3000',
documentUrl: 'http://nas.7bm.co:9000/docauditai/',
uploadUrl: 'http://172.16.0.58:8008/admin/documents/upload',
},
// 生产环境
production: {
baseUrl: 'http://nas.7bm.co:3000',
documentUrl: 'http://nas.7bm.co:9000/docauditai/',
uploadUrl: 'http://172.16.0.58:8008/admin/documents/upload',
},
// 备用配置 (可以根据需要添加更多环境)
staging: {
baseUrl: 'http://172.16.0.119:9000/admin',
documentUrl: 'http://nas.7bm.co:9000/docauditai/',
uploadUrl: 'http://172.16.0.119:8000/admin/documents/upload',
}
};
// 获取当前环境,默认为development
const getCurrentEnvironment = (): string => {
// 优先使用环境变量,然后使用 NODE_ENV
return process.env.NEXT_PUBLIC_API_ENV || process.env.NODE_ENV || 'development';
};
// 从环境变量获取配置,如果环境变量不存在则使用默认配置
const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => {
return {
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || defaultConfig.baseUrl,
documentUrl: process.env.NEXT_PUBLIC_DOCUMENT_URL || defaultConfig.documentUrl,
uploadUrl: process.env.NEXT_PUBLIC_UPLOAD_URL || defaultConfig.uploadUrl,
postgrestUrl: process.env.NEXT_PUBLIC_POSTGREST_URL || defaultConfig.postgrestUrl,
};
};
// 获取当前配置
const getCurrentConfig = (): ApiConfig => {
const env = getCurrentEnvironment();
const defaultConfig = configs[env] || configs.development;
// 如果是浏览器环境,尝试从环境变量覆盖配置
if (typeof window !== 'undefined' || process.env.NEXT_PUBLIC_API_BASE_URL) {
return getConfigFromEnv(defaultConfig);
}
return defaultConfig;
};
// 导出当前环境的配置
export const apiConfig = getCurrentConfig();
// 导出具体的配置项,方便使用
export const {
baseUrl: API_BASE_URL,
documentUrl: DOCUMENT_URL,
uploadUrl: UPLOAD_URL,
postgrestUrl: POSTGREST_URL
} = apiConfig;
// 导出所有配置,供调试使用
export { configs };
// 工具函数:设置环境(主要用于测试)
export const setEnvironment = (env: string): ApiConfig => {
return configs[env] || configs.development;
};
// 调试信息(仅在开发环境显示)
// if (process.env.NODE_ENV === 'development') {
console.log('📦 API配置信息:', {
environment: getCurrentEnvironment(),
config: apiConfig
});
// }
+1 -1
View File
@@ -197,7 +197,7 @@ export function links() {
{ rel: "stylesheet", href: messageModalStyles },
{ rel: "stylesheet", href: toastStyles },
// 添加 Antd 样式
{ rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/antd@5/dist/reset.css" },
// { rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/antd@5/dist/reset.css" },
{ rel: "icon", type: "image/svg+xml", href: "/logo.svg" },
// { rel: "preconnect", href: "https://fonts.googleapis.com" },
// { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },