进度条优化

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
-76
View File
@@ -1,76 +0,0 @@
import { apiRequest, buildUrl, type PaginatedResponse } from './base';
import type { ReviewResult, RuleCheckResult, AIAnalysis } from '~/models/review';
/**
* 评查结果API服务
*/
interface ReviewFilterParams {
fileId?: string;
reviewStatus?: string;
startDate?: string;
endDate?: string;
keyword?: string;
page?: number;
pageSize?: number;
}
// 获取评查结果列表
export async function getReviews(params?: ReviewFilterParams): Promise<PaginatedResponse<ReviewResult>> {
const url = buildUrl('/api/reviews', params);
return apiRequest<PaginatedResponse<ReviewResult>>(url);
}
// 获取单个评查结果
export async function getReview(id: string): Promise<ReviewResult> {
const url = buildUrl(`/api/reviews/${id}`);
return apiRequest<ReviewResult>(url);
}
// 获取评查点结果列表
export async function getReviewPoints(reviewId: string): Promise<RuleCheckResult[]> {
const url = buildUrl(`/api/reviews/${reviewId}/points`);
return apiRequest<RuleCheckResult[]>(url);
}
// 开始评查
export async function startReview(fileId: string): Promise<ReviewResult> {
const url = buildUrl(`/api/reviews/start/${fileId}`);
return apiRequest<ReviewResult>(url, 'POST');
}
// 更新评查点结果
export async function updateReviewPoint(reviewId: string, pointId: string, data: Partial<RuleCheckResult>): Promise<RuleCheckResult> {
const url = buildUrl(`/api/reviews/${reviewId}/points/${pointId}`);
return apiRequest<RuleCheckResult>(url, 'PUT', data);
}
// 完成评查
export async function completeReview(reviewId: string): Promise<ReviewResult> {
const url = buildUrl(`/api/reviews/${reviewId}/complete`);
return apiRequest<ReviewResult>(url, 'POST');
}
// 获取AI分析
export async function getAIAnalysis(reviewId: string): Promise<AIAnalysis> {
const url = buildUrl(`/api/reviews/${reviewId}/analysis`);
return apiRequest<AIAnalysis>(url);
}
// 导出评查报告
export async function exportReviewReport(reviewId: string, format: 'pdf' | 'word' = 'pdf'): Promise<Blob> {
const url = buildUrl(`/api/reviews/${reviewId}/export`, { format });
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': format === 'pdf' ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}
});
if (!response.ok) {
throw new Error('导出评查报告失败');
}
return response.blob();
}
+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;
+2
View File
@@ -20,6 +20,7 @@ import styles from "~/styles/main.css?url";
import messageModalStyles from "~/styles/components/message-modal.css?url";
import toastStyles from "~/styles/components/toast.css?url";
import LoadingBarContainer from "~/components/ui/LoadingBar";
import RouteChangeLoader from "~/components/ui/RouteChangeLoader";
// 添加客户端hydration错误处理
// if (typeof window !== "undefined") {
@@ -84,6 +85,7 @@ export default function App() {
<Layout>
<Outlet />
</Layout>
<RouteChangeLoader />
</ToastProvider>
</MessageModalProvider>
<ScrollRestoration />
-7
View File
@@ -586,25 +586,18 @@ export default function DocumentsIndex() {
// 检查audit_status是否为0,如果是则更新为2
if (auditStatus === 0 || auditStatus === null) {
try {
// 显示加载状态
loadingBarService.show();
const response = await updateDocumentAuditStatus(fileId.toString(), 2);
if (response.error) {
console.error('更新文件审核状态失败:', response.error);
toastService.error('更新文件审核状态失败:' + (response.error || '未知错误'));
loadingBarService.hide();
return;
}
} catch (error) {
console.error('更新文件审核状态时出错:', error);
toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
loadingBarService.hide();
return;
}
} else {
// 显示加载状态
loadingBarService.show();
}
// 导航到评查详情页
+192
View File
@@ -0,0 +1,192 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { Spin, Tooltip, Input } from 'antd';
import {
LeftOutlined,
RightOutlined,
PlusCircleOutlined,
MinusCircleOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
RotateLeftOutlined,
RotateRightOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import './index.less';
import { Document, Page, pdfjs } from 'react-pdf';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
const PDFView = ({
file,
parentDom,
onClose,
}: {
file?: string | null;
parentDom?: HTMLDivElement | null;
onClose?: () => void;
}) => {
const defaultWidth = 600;
const pageDiv = useRef<HTMLDivElement>(null);
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
const [pageWidth, setPageWidth] = useState<number>(defaultWidth);
const [fullscreen, setFullscreen] = useState<boolean>(false);
const [rotation, setRotation] = useState<number>(0);
const [showThumbnails, setShowThumbnails] = useState<boolean>(false);
const [visiblePages, setVisiblePages] = useState<number[]>([1]); // 控制可见页面
const parent = parentDom || document.body;
// 加载 PDF 元信息,不渲染全部页面
const onDocumentLoadSuccess = useCallback(({ numPages }: { numPages: number }) => {
setNumPages(numPages);
}, []);
const lastPage = () => pageNumber > 1 && setPageNumber(pageNumber - 1);
const nextPage = () => pageNumber < numPages && setPageNumber(pageNumber + 1);
const onPageNumberChange = (e: { target: { value: string } }) => {
let value = Math.max(1, Math.min(numPages, Number(e.target.value) || 1));
setPageNumber(value);
setVisiblePages([value]); // 只加载当前页
};
const pageZoomIn = () => setPageWidth(pageWidth * 1.2);
const pageZoomOut = () => pageWidth > defaultWidth && setPageWidth(pageWidth * 0.8);
const pageFullscreen = () => {
setPageWidth(fullscreen ? defaultWidth : parent.offsetWidth - 50);
setFullscreen(!fullscreen);
};
const rotateLeft = () => setRotation((prev) => (prev - 90) % 360);
const rotateRight = () => setRotation((prev) => (prev + 90) % 360);
const toggleThumbnails = () => setShowThumbnails(!showThumbnails);
// 动态更新可见页面
useEffect(() => {
if (!showThumbnails) {
setVisiblePages([pageNumber]);
} else {
// 缩略图模式下限制加载数量,避免卡顿
const start = Math.max(1, pageNumber - 2);
const end = Math.min(numPages, pageNumber + 2);
setVisiblePages(Array.from({ length: end - start + 1 }, (_, i) => start + i));
}
}, [pageNumber, showThumbnails, numPages]);
useEffect(() => setPageNumber(1), [file]);
useEffect(() => {
if( pageDiv.current){
(pageDiv.current.scrollTop = 0)
}
}, [pageNumber]);
const renderContent=()=>(<div className='view'>
<div className='viewContent' >
<div className='pageMain' ref={pageDiv}>
<div className='pageContainer'>
<Document
file={file}
onLoadSuccess={onDocumentLoadSuccess}
error={
<div style={{ textAlign: 'center', width: defaultWidth + 'px' }}>
<ExclamationCircleOutlined style={{ fontSize: '150px', color: '#fe725c', margin: '100px' }} />
</div>
}
loading={<div style={{ textAlign: 'center', width: defaultWidth + 'px' }}><Spin size="large" style={{ margin: '200px' }} /></div>}
>
{showThumbnails ? (
<div className='thumbnailContainer'>
{Array.from({ length: numPages }, (_, i) => i + 1).map((page) => (
<div
key={page}
className='thumbnail'
onClick={() => {
setPageNumber(page);
setShowThumbnails(false);
}}
>
{visiblePages.includes(page) ? (
<Page
pageNumber={page}
width={150}
rotate={rotation}
loading={<Spin />}
renderTextLayer={false} // 禁用文本层,提升性能
renderAnnotationLayer={false} // 禁用注释层
/>
) : (
<div className='thumbnailPlaceholder'> {page} </div>
)}
<span> {page} </span>
</div>
))}
</div>
) : (
<Page
pageNumber={pageNumber}
width={pageWidth}
rotate={rotation}
loading={<Spin size="large" />}
renderTextLayer={false} // 禁用文本层
renderAnnotationLayer={false} // 禁用注释层
error={() => setPageNumber(1)}
/>
)}
</Document>
</div>
</div>
<div className='pageBar'>
<div className='pageTool'>
<Tooltip title={pageNumber === 1 ? '已是第一页' : '上一页'}>
<LeftOutlined onClick={lastPage} />
</Tooltip>
<Input
value={pageNumber}
onChange={onPageNumberChange}
onPressEnter={onPageNumberChange as any}
type="number"
/>{' '}
/ {numPages}
<Tooltip title={pageNumber === numPages ? '已是最后一页' : '下一页'}>
<RightOutlined onClick={nextPage} />
</Tooltip>
<Tooltip title="放大">
<PlusCircleOutlined onClick={pageZoomIn} />
</Tooltip>
<Tooltip title="缩小">
<MinusCircleOutlined onClick={pageZoomOut} />
</Tooltip>
<Tooltip title="向左旋转">
<RotateLeftOutlined onClick={rotateLeft} />
</Tooltip>
<Tooltip title="向右旋转">
<RotateRightOutlined onClick={rotateRight} />
</Tooltip>
<Tooltip title={showThumbnails ? '关闭缩略图' : '显示缩略图'}>
<UnorderedListOutlined onClick={toggleThumbnails} />
</Tooltip>
<Tooltip title={fullscreen ? '恢复默认' : '适合窗口'}>
{fullscreen ? <FullscreenExitOutlined onClick={pageFullscreen} /> : <FullscreenOutlined onClick={pageFullscreen} />}
</Tooltip>
{onClose && (
<Tooltip title="关闭">
<CloseCircleOutlined onClick={onClose} />
</Tooltip>
)}
</div>
</div>
</div>
</div>)
if(parentDom){
return renderContent()
}
return createPortal(
renderContent(),
parent,)
};
export default PDFView;
+2 -2
View File
@@ -45,8 +45,7 @@ import {
// 从ReviewPointsList组件中导入ReviewPoint类型
import { type ReviewPoint } from '~/components/reviews';
import { messageService } from "~/components/ui/MessageModal";
import { Button } from "~/components/ui/Button";
import { loadingBarService } from "~/components/ui/LoadingBar";
/**
* 文件信息组件
@@ -248,6 +247,7 @@ export default function ReviewDetails() {
// loader 数据加载出错
useEffect(()=>{
loadingBarService.hide();
if(Object.keys(loaderData).find(key => key === 'result') && !loaderData.result){
messageService.show({
title: '错误',