进度条优化
This commit is contained in:
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useNavigate } from "@remix-run/react";
|
import { useNavigate } from "@remix-run/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { loadingBarService } from "~/components/ui/LoadingBar";
|
||||||
|
|
||||||
interface FileInfoProps {
|
interface FileInfoProps {
|
||||||
fileInfo: {
|
fileInfo: {
|
||||||
@@ -65,6 +66,7 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
|||||||
|
|
||||||
// 设置导航状态为true
|
// 设置导航状态为true
|
||||||
setIsNavigating(true);
|
setIsNavigating(true);
|
||||||
|
loadingBarService.show();
|
||||||
|
|
||||||
// 根据来源页面返回
|
// 根据来源页面返回
|
||||||
const previousRoute = fileInfo.previousRoute || 'documents';
|
const previousRoute = fileInfo.previousRoute || 'documents';
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ interface FileContent {
|
|||||||
|
|
||||||
interface FilePreviewProps {
|
interface FilePreviewProps {
|
||||||
fileContent: FileContent;
|
fileContent: FileContent;
|
||||||
reviewPoints: ReviewPoint[];
|
reviewPoints?: ReviewPoint[]; // 设为可选
|
||||||
activeReviewPointResultId: string | null;
|
activeReviewPointResultId: string | null;
|
||||||
targetPage?: number; // 新增目标页码参数
|
targetPage?: number; // 新增目标页码参数
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,12 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
const [loadError, setLoadError] = useState<string | null>(null);
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
const [pageInputValue, setPageInputValue] = useState<string>('');
|
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 = () => {
|
const handleZoomIn = () => {
|
||||||
if (zoomLevel < 200) {
|
if (zoomLevel < 200) {
|
||||||
@@ -93,27 +99,75 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换高亮显示
|
// 切换拖拽模式
|
||||||
// const toggleHighlights = () => {
|
const toggleDragMode = () => {
|
||||||
// setHighlightsVisible(!highlightsVisible);
|
setDragMode(prev => !prev);
|
||||||
// };
|
setDragCursor(prev => prev === 'default' ? 'grab' : 'default');
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
// 当选中的评查点变化时,滚动到对应位置
|
// 处理拖拽开始
|
||||||
// useEffect(() => {
|
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
// if (activeReviewPointId && contentRef.current) {
|
if (!dragMode || e.button !== 0) return; // 只在拖拽模式下响应左键点击
|
||||||
// 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');
|
e.preventDefault();
|
||||||
// setTimeout(() => {
|
|
||||||
// highlightElement.classList.remove('highlight-focus');
|
// 设置拖拽状态
|
||||||
// }, 1500);
|
setIsDragging(true);
|
||||||
// }
|
setDragCursor('grabbing');
|
||||||
// }
|
|
||||||
// }, [activeReviewPointId]);
|
// 记录鼠标初始位置
|
||||||
|
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);
|
const prevTargetPageRef = useRef<number | undefined>(undefined);
|
||||||
@@ -130,6 +184,7 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("访问ocrResult时出错:", error);
|
console.error("访问ocrResult时出错:", error);
|
||||||
|
toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageElement = document.getElementById(`page-${newTargetPage}`);
|
const pageElement = document.getElementById(`page-${newTargetPage}`);
|
||||||
@@ -230,11 +285,6 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
|
|
||||||
// 遍历每一页,生成对应的页面组件
|
// 遍历每一页,生成对应的页面组件
|
||||||
for (let i = 1; i <= numPages; i++) {
|
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 zoomFactor = zoomLevel / 100;
|
||||||
const pageContainerStyle = {
|
const pageContainerStyle = {
|
||||||
@@ -250,12 +300,13 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
|
|
||||||
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
|
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
|
||||||
<div
|
<div
|
||||||
className="page-wrapper flex justify-center"
|
className="page-wrapper"
|
||||||
style={{
|
style={{
|
||||||
transform: `scale(${zoomFactor})`, // 根据zoomLevel应用缩放
|
transform: `scale(${zoomFactor})`, // 根据zoomLevel应用缩放
|
||||||
transformOrigin: 'top center', // 缩放原点设置为顶部中心
|
transformOrigin: 'top center', // 缩放原点设置为顶部中心
|
||||||
position: 'relative', // 相对定位,作为评查点高亮的定位参考
|
position: 'relative', // 相对定位,作为评查点高亮的定位参考
|
||||||
maxWidth: '100%', // 限制最大宽度
|
display: 'inline-block', // 内联块级元素,宽度由内容决定
|
||||||
|
margin: '0 auto', // 水平居中
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 渲染PDF页面组件 */}
|
{/* 渲染PDF页面组件 */}
|
||||||
@@ -305,7 +356,14 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
// 渲染文档内容
|
// 渲染文档内容
|
||||||
const renderDocumentContent = () => {
|
const renderDocumentContent = () => {
|
||||||
return (
|
return (
|
||||||
<div style={styles.pdfContainer}>
|
<div
|
||||||
|
style={{
|
||||||
|
...styles.pdfContainer,
|
||||||
|
// 当缩放大于100%时设置最小宽度,确保出现横向滚动条
|
||||||
|
minWidth: zoomLevel > 100 ? `${zoomLevel}%` : '100%',
|
||||||
|
overflow: 'visible'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Document
|
<Document
|
||||||
file={'http://172.18.0.100:9000/docauditai/'+fileContent.path}
|
file={'http://172.18.0.100:9000/docauditai/'+fileContent.path}
|
||||||
onLoadSuccess={onDocumentLoadSuccess}
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
@@ -313,7 +371,7 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
console.error("PDF加载错误:", error);
|
console.error("PDF加载错误:", error);
|
||||||
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
|
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
|
||||||
}}
|
}}
|
||||||
className="flex flex-col items-center w-full"
|
className="w-full"
|
||||||
error={<div className="text-red-500">PDF文档加载失败,请检查链接或网络连接。</div>}
|
error={<div className="text-red-500">PDF文档加载失败,请检查链接或网络连接。</div>}
|
||||||
noData={<div>无数据</div>}
|
noData={<div>无数据</div>}
|
||||||
loading={<div className="text-center py-10">PDF加载中...</div>}
|
loading={<div className="text-center py-10">PDF加载中...</div>}
|
||||||
@@ -378,12 +436,61 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
<i className="ri-mark-pen-line"></i> {highlightsVisible ? '隐藏问题' : '显示问题'}
|
<i className="ri-mark-pen-line"></i> {highlightsVisible ? '隐藏问题' : '显示问题'}
|
||||||
</button> */}
|
</button> */}
|
||||||
<span className="ml-2 text-xs text-gray-500 inline-block">{"比例:"+zoomLevel+"%"}</span>
|
<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>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="file-preview-content relative overflow-y-auto"
|
className="file-preview-content"
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
style={{ maxHeight: 'calc(100vh - 150px)' }}
|
style={{
|
||||||
|
maxHeight: 'calc(100vh - 150px)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'auto',
|
||||||
|
cursor: dragCursor,
|
||||||
|
userSelect: dragMode ? 'none' : 'auto', // 拖拽模式下防止文本被选中
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 ? (
|
{loadError ? (
|
||||||
<div className="text-red-500 p-4">
|
<div className="text-red-500 p-4">
|
||||||
@@ -392,6 +499,7 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult
|
|||||||
) : (
|
) : (
|
||||||
renderDocumentContent()
|
renderDocumentContent()
|
||||||
)}
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -559,53 +559,6 @@ export function ReviewPointsList({
|
|||||||
{/* 评查点内容显示区域 */}
|
{/* 评查点内容显示区域 */}
|
||||||
{reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
|
{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">
|
<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)}
|
{renderContent(reviewPoint)}
|
||||||
</div>
|
</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 className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||||
<div>
|
<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)}
|
{renderContent(reviewPoint)}
|
||||||
</div>
|
</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 className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||||
<div>
|
<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)}
|
{renderContent(reviewPoint)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -20,6 +20,7 @@ import styles from "~/styles/main.css?url";
|
|||||||
import messageModalStyles from "~/styles/components/message-modal.css?url";
|
import messageModalStyles from "~/styles/components/message-modal.css?url";
|
||||||
import toastStyles from "~/styles/components/toast.css?url";
|
import toastStyles from "~/styles/components/toast.css?url";
|
||||||
import LoadingBarContainer from "~/components/ui/LoadingBar";
|
import LoadingBarContainer from "~/components/ui/LoadingBar";
|
||||||
|
import RouteChangeLoader from "~/components/ui/RouteChangeLoader";
|
||||||
|
|
||||||
// 添加客户端hydration错误处理
|
// 添加客户端hydration错误处理
|
||||||
// if (typeof window !== "undefined") {
|
// if (typeof window !== "undefined") {
|
||||||
@@ -84,6 +85,7 @@ export default function App() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<RouteChangeLoader />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</MessageModalProvider>
|
</MessageModalProvider>
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
|
|||||||
@@ -586,25 +586,18 @@ export default function DocumentsIndex() {
|
|||||||
// 检查audit_status是否为0,如果是则更新为2
|
// 检查audit_status是否为0,如果是则更新为2
|
||||||
if (auditStatus === 0 || auditStatus === null) {
|
if (auditStatus === 0 || auditStatus === null) {
|
||||||
try {
|
try {
|
||||||
// 显示加载状态
|
|
||||||
loadingBarService.show();
|
|
||||||
|
|
||||||
const response = await updateDocumentAuditStatus(fileId.toString(), 2);
|
const response = await updateDocumentAuditStatus(fileId.toString(), 2);
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
console.error('更新文件审核状态失败:', response.error);
|
console.error('更新文件审核状态失败:', response.error);
|
||||||
toastService.error('更新文件审核状态失败:' + (response.error || '未知错误'));
|
toastService.error('更新文件审核状态失败:' + (response.error || '未知错误'));
|
||||||
loadingBarService.hide();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新文件审核状态时出错:', error);
|
console.error('更新文件审核状态时出错:', error);
|
||||||
toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
|
toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
|
||||||
loadingBarService.hide();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 显示加载状态
|
|
||||||
loadingBarService.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导航到评查详情页
|
// 导航到评查详情页
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -45,8 +45,7 @@ import {
|
|||||||
// 从ReviewPointsList组件中导入ReviewPoint类型
|
// 从ReviewPointsList组件中导入ReviewPoint类型
|
||||||
import { type ReviewPoint } from '~/components/reviews';
|
import { type ReviewPoint } from '~/components/reviews';
|
||||||
import { messageService } from "~/components/ui/MessageModal";
|
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 数据加载出错
|
// loader 数据加载出错
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
|
loadingBarService.hide();
|
||||||
if(Object.keys(loaderData).find(key => key === 'result') && !loaderData.result){
|
if(Object.keys(loaderData).find(key => key === 'result') && !loaderData.result){
|
||||||
messageService.show({
|
messageService.show({
|
||||||
title: '错误',
|
title: '错误',
|
||||||
|
|||||||
Reference in New Issue
Block a user