进度条优化

This commit is contained in:
2025-04-25 14:12:36 +08:00
parent 0eaaa5b041
commit b9b5be1d47
9 changed files with 377 additions and 270 deletions
+2
View File
@@ -1,5 +1,6 @@
import { useNavigate } from "@remix-run/react";
import { useState } from "react";
import { loadingBarService } from "~/components/ui/LoadingBar";
interface FileInfoProps {
fileInfo: {
@@ -65,6 +66,7 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
// 设置导航状态为true
setIsNavigating(true);
loadingBarService.show();
// 根据来源页面返回
const previousRoute = fileInfo.previousRoute || 'documents';
+148 -40
View File
@@ -66,7 +66,7 @@ interface FileContent {
interface FilePreviewProps {
fileContent: FileContent;
reviewPoints: ReviewPoint[];
reviewPoints?: ReviewPoint[]; // 设为可选
activeReviewPointResultId: string | null;
targetPage?: number; // 新增目标页码参数
}
@@ -79,6 +79,12 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
const [loadError, setLoadError] = useState<string | null>(null);
const [pageInputValue, setPageInputValue] = useState<string>('');
// 拖拽状态管理
const [dragMode, setDragMode] = useState(false); // 是否处于拖拽模式
const [isDragging, setIsDragging] = useState(false);
const [dragCursor, setDragCursor] = useState('default');
const lastMousePosRef = useRef({ x: 0, y: 0 });
// 放大文档
const handleZoomIn = () => {
if (zoomLevel < 200) {
@@ -93,27 +99,75 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
}
};
// 切换高亮显示
// const toggleHighlights = () => {
// setHighlightsVisible(!highlightsVisible);
// };
// 切换拖拽模式
const toggleDragMode = () => {
setDragMode(prev => !prev);
setDragCursor(prev => prev === 'default' ? 'grab' : 'default');
setIsDragging(false);
};
// 当选中的评查点变化时,滚动到对应位置
// useEffect(() => {
// if (activeReviewPointId && contentRef.current) {
// const highlightElement = contentRef.current.querySelector(`[data-review-id="${activeReviewPointId}"]`);
// if (highlightElement) {
// highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// // highlightElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
// // 添加临时突出显示效果
// highlightElement.classList.add('highlight-focus');
// setTimeout(() => {
// highlightElement.classList.remove('highlight-focus');
// }, 1500);
// }
// }
// }, [activeReviewPointId]);
// 处理拖拽开始
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!dragMode || e.button !== 0) return; // 只在拖拽模式下响应左键点击
// 防止选中文本
e.preventDefault();
// 设置拖拽状态
setIsDragging(true);
setDragCursor('grabbing');
// 记录鼠标初始位置
lastMousePosRef.current = {
x: e.clientX,
y: e.clientY
};
};
// 处理拖拽过程
const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!dragMode || !isDragging || !contentRef.current) return;
// 计算鼠标移动距离
const dx = e.clientX - lastMousePosRef.current.x;
const dy = e.clientY - lastMousePosRef.current.y;
// 更新容器滚动位置
contentRef.current.scrollLeft -= dx;
contentRef.current.scrollTop -= dy;
// 更新鼠标位置记录
lastMousePosRef.current = {
x: e.clientX,
y: e.clientY
};
};
// 处理拖拽结束
const handleMouseUp = () => {
if (!dragMode) return;
setIsDragging(false);
setDragCursor('grab');
};
// 监听鼠标离开窗口事件
useEffect(() => {
const handleMouseLeave = () => {
if (dragMode && isDragging) {
setIsDragging(false);
setDragCursor('grab');
}
};
document.addEventListener('mouseleave', handleMouseLeave);
document.addEventListener('mouseup', handleMouseUp as EventListener);
return () => {
document.removeEventListener('mouseleave', handleMouseLeave);
document.removeEventListener('mouseup', handleMouseUp as EventListener);
};
}, [isDragging, dragMode]);
// 处理页面跳转
const prevTargetPageRef = useRef<number | undefined>(undefined);
@@ -130,6 +184,7 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
}
} catch (error) {
console.error("访问ocrResult时出错:", error);
toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误'));
}
const pageElement = document.getElementById(`page-${newTargetPage}`);
@@ -230,11 +285,6 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
// 遍历每一页,生成对应的页面组件
for (let i = 1; i <= numPages; i++) {
// 查找该页面上的评查点,基于position.section匹配页面ID
// const pageReviewPoints = reviewPoints.filter(point =>
// point.position && point.position.section === `page-${i}`
// );
// 计算当前缩放级别下的页面容器样式
const zoomFactor = zoomLevel / 100;
const pageContainerStyle = {
@@ -250,12 +300,13 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
<div
className="page-wrapper flex justify-center"
className="page-wrapper"
style={{
transform: `scale(${zoomFactor})`, // 根据zoomLevel应用缩放
transformOrigin: 'top center', // 缩放原点设置为顶部中心
position: 'relative', // 相对定位,作为评查点高亮的定位参考
maxWidth: '100%', // 限制最大宽度
display: 'inline-block', // 内联块级元素,宽度由内容决定
margin: '0 auto', // 水平居中
}}
>
{/* 渲染PDF页面组件 */}
@@ -305,7 +356,14 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
// 渲染文档内容
const renderDocumentContent = () => {
return (
<div style={styles.pdfContainer}>
<div
style={{
...styles.pdfContainer,
// 当缩放大于100%时设置最小宽度,确保出现横向滚动条
minWidth: zoomLevel > 100 ? `${zoomLevel}%` : '100%',
overflow: 'visible'
}}
>
<Document
file={'http://172.18.0.100:9000/docauditai/'+fileContent.path}
onLoadSuccess={onDocumentLoadSuccess}
@@ -313,7 +371,7 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
console.error("PDF加载错误:", error);
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
}}
className="flex flex-col items-center w-full"
className="w-full"
error={<div className="text-red-500">PDF文档加载失败</div>}
noData={<div></div>}
loading={<div className="text-center py-10">PDF加载中...</div>}
@@ -336,7 +394,7 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5 "
onClick={handleScrollToTop}
>
<i className="ri-arrow-up-double-line "></i>
<i className="ri-arrow-up-double-line"></i>
<span className="ml-1"></span>
</button>
<button
@@ -378,20 +436,70 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
<i className="ri-mark-pen-line"></i> {highlightsVisible ? '隐藏问题' : '显示问题'}
</button> */}
<span className="ml-2 text-xs text-gray-500 inline-block">{"比例:"+zoomLevel+"%"}</span>
<button
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5 ml-2 ${dragMode ? 'active bg-green-300' : ''}`}
title="切换拖拽模式"
aria-pressed={dragMode}
onClick={toggleDragMode}
>
<i className="ri-drag-move-line"></i>
<span className="ml-1">{dragMode ? '(已激活)' : ''}</span>
</button>
</div>
</div>
<div
className="file-preview-content relative overflow-y-auto"
className="file-preview-content"
ref={contentRef}
style={{ maxHeight: 'calc(100vh - 150px)' }}
style={{
maxHeight: 'calc(100vh - 150px)',
overflowY: 'auto',
overflowX: 'auto',
cursor: dragCursor,
userSelect: dragMode ? 'none' : 'auto', // 拖拽模式下防止文本被选中
}}
>
{loadError ? (
<div className="text-red-500 p-4">
<p>{loadError}</p>
</div>
) : (
renderDocumentContent()
)}
<button
className="pdf-interactive-container"
aria-label="PDF文档查看区域"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onKeyDown={(e) => {
// 添加键盘导航支持
const scrollAmount = 50;
if (e.key === 'ArrowUp') {
if (contentRef.current) contentRef.current.scrollTop -= scrollAmount;
e.preventDefault();
} else if (e.key === 'ArrowDown') {
if (contentRef.current) contentRef.current.scrollTop += scrollAmount;
e.preventDefault();
} else if (e.key === 'ArrowLeft') {
if (contentRef.current) contentRef.current.scrollLeft -= scrollAmount;
e.preventDefault();
} else if (e.key === 'ArrowRight') {
if (contentRef.current) contentRef.current.scrollLeft += scrollAmount;
e.preventDefault();
}
}}
style={{
position: 'relative',
height: '100%',
width: '100%',
display: 'block',
border: 'none',
background: 'transparent',
textAlign: 'center',
padding: 0
}}
>
{loadError ? (
<div className="text-red-500 p-4">
<p>{loadError}</p>
</div>
) : (
renderDocumentContent()
)}
</button>
</div>
</div>
);
-145
View File
@@ -559,53 +559,6 @@ export function ReviewPointsList({
{/* 评查点内容显示区域 */}
{reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
{/* 修改评查结果的结构之前,先显示旧的结构 */}
{/* {Object.entries(reviewPoint.content).map(([key, value], index) => (
<div
key={index}
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
onClick={(e) => {
// 阻止事件冒泡,防止触发父元素的点击事件
e.stopPropagation();
console.log(`通过:单独点击${key}----`, reviewPoint);
// 检查评查点是否有 contentPage 以及当前 key 对应的页码数组
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
// 获取当前 key 对应的第一个页码并跳转
console.log(`通过:单独点击${key}----页码---`, reviewPoint.contentPage[key][0]);
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
} else {
console.log(`通过:单独点击${key}--------没有对应页码`);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
} else {
console.log(`通过:单独点击${key}--------没有对应页码`);
}
}
}}
role="button"
tabIndex={0}
aria-label={`查看${key}内容详情`}
>
<div className="flex justify-between items-center mb-1">
<span className="text-xs">{key}</span>
<span className={`text-xs w-15 ${value ? 'text-error' : 'text-warning'}`}>
{value ? '' : '缺失'}
</span>
</div>
<p className="text-xs text-left select-text">
{typeof value === 'object' && value !== null
? (value.value || (value.value === '' ? <span className="invisible">占位符</span> : ''))
: (value || (value === '' ? <span className="invisible">占位符</span> : ''))}
</p>
</div>
))} */}
{/* 修改评查结果的结构之后,显示新的结构 */}
{renderContent(reviewPoint)}
</div>
@@ -662,54 +615,6 @@ export function ReviewPointsList({
)}
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
<div>
{/* 修改评查结果的结构之前,先显示旧的结构 */}
{/* {Object.entries(reviewPoint.content).map(([key, value], index) => (
<div
key={index}
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
onClick={(e) => {
// 阻止事件冒泡,防止触发父元素的点击事件
e.stopPropagation();
console.log(`通过:单独点击${key}----`, reviewPoint);
// 检查评查点是否有 contentPage 以及当前 key 对应的页码数组
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
// 获取当前 key 对应的第一个页码并跳转
console.log(`通过:单独点击${key}----页码---`, reviewPoint.contentPage[key][0]);
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
} else {
console.log(`通过:单独点击${key}--------没有对应页码`);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
} else {
console.log(`通过:单独点击${key}--------没有对应页码`);
}
}
}}
role="button"
tabIndex={0}
aria-label={`查看${key}内容详情`}
>
<div className="flex justify-between items-center mb-1">
<span className="text-xs">{key}</span>
<span className={`text-xs w-15 ${value ? 'text-error' : 'text-warning'}`}>
{value ? '' : '缺失'}
</span>
</div>
<p className="text-xs text-left select-text">
{typeof value === 'object' && value !== null
? (value.value || (value.value === '' ? <span className="invisible">占位符</span> : ''))
: (value || (value === '' ? <span className="invisible">占位符</span> : ''))}
</p>
</div>
))} */}
{/* 修改评查结果的结构之后,显示新的结构 */}
{renderContent(reviewPoint)}
</div>
@@ -777,56 +682,6 @@ export function ReviewPointsList({
{/* 内容显示区域 */}
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
<div>
{/* 修改评查结果的结构之前,先显示旧的结构 */}
{/* {Object.entries(reviewPoint.content).map(([key, value], index) => (
<div
key={index}
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1"
onClick={(e) => {
// 阻止事件冒泡,防止触发父元素的点击事件
e.stopPropagation();
console.log(`非通过:单独点击${key}----`, reviewPoint);
// 检查评查点是否有 contentPage 以及当前 key 对应的页码数组
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
// 获取当前 key 对应的第一个页码并跳转
console.log(`非通过:单独点击${key}----页码---`, reviewPoint.contentPage[key][0]);
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
} else {
// 如果没有对应页码,弹出提示
// alert(`无法找到"${key}"对应的内容页面`);
console.log(`非通过:单独点击${key}--------没有对应页码`);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (reviewPoint.contentPage && reviewPoint.contentPage[key] && reviewPoint.contentPage[key].length > 0) {
onReviewPointSelect(reviewPoint.id, reviewPoint.contentPage[key][0]);
} else {
// alert(`无法找到"${key}"对应的内容页面`);
console.log(`非通过:单独点击${key}--------没有对应页码`);
}
}
}}
role="button"
tabIndex={0}
aria-label={`查看${key}内容详情`}
>
<div className="flex justify-between items-center mb-1">
<span className="text-xs">{key}</span>
<span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
{value ? '' : '缺失'}
</span>
</div>
<p className="text-xs text-left select-text">
{typeof value === 'object' && value !== null
? (value.value || (value.value === '' ? <span className="invisible">占位符</span> : ''))
: (value || (value === '' ? <span className="invisible">占位符</span> : ''))}
</p>
</div>
))} */}
{/* 修改评查结果的结构之后,显示新的结构 */}
{renderContent(reviewPoint)}
</div>
+31
View File
@@ -0,0 +1,31 @@
import { useEffect } from 'react';
import { useNavigation } from '@remix-run/react';
import { loadingBarService } from './LoadingBar';
/**
* 路由变化加载器组件
* 用于监听路由变化并控制全局加载进度条的显示
*/
export function RouteChangeLoader() {
// 获取路由转换状态
const navigation = useNavigation();
const isNavigating =
navigation.state === 'loading' ||
navigation.state === 'submitting';
// 监听路由变化状态,控制加载进度条
useEffect(() => {
// 当开始导航时,显示加载进度条
if (isNavigating) {
loadingBarService.show();
} else {
// 当导航完成时,隐藏加载进度条(带完成动画)
loadingBarService.hide();
}
}, [isNavigating]);
// 这个组件不渲染任何内容,仅监听路由变化
return null;
}
export default RouteChangeLoader;