diff --git a/app/api/reviewsApi.ts b/app/api/reviewsApi.ts deleted file mode 100644 index 7cf6f07..0000000 --- a/app/api/reviewsApi.ts +++ /dev/null @@ -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> { - const url = buildUrl('/api/reviews', params); - return apiRequest>(url); -} - -// 获取单个评查结果 -export async function getReview(id: string): Promise { - const url = buildUrl(`/api/reviews/${id}`); - return apiRequest(url); -} - -// 获取评查点结果列表 -export async function getReviewPoints(reviewId: string): Promise { - const url = buildUrl(`/api/reviews/${reviewId}/points`); - return apiRequest(url); -} - -// 开始评查 -export async function startReview(fileId: string): Promise { - const url = buildUrl(`/api/reviews/start/${fileId}`); - return apiRequest(url, 'POST'); -} - -// 更新评查点结果 -export async function updateReviewPoint(reviewId: string, pointId: string, data: Partial): Promise { - const url = buildUrl(`/api/reviews/${reviewId}/points/${pointId}`); - return apiRequest(url, 'PUT', data); -} - -// 完成评查 -export async function completeReview(reviewId: string): Promise { - const url = buildUrl(`/api/reviews/${reviewId}/complete`); - return apiRequest(url, 'POST'); -} - -// 获取AI分析 -export async function getAIAnalysis(reviewId: string): Promise { - const url = buildUrl(`/api/reviews/${reviewId}/analysis`); - return apiRequest(url); -} - -// 导出评查报告 -export async function exportReviewReport(reviewId: string, format: 'pdf' | 'word' = 'pdf'): Promise { - 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(); -} \ No newline at end of file diff --git a/app/components/reviews/FileInfo.tsx b/app/components/reviews/FileInfo.tsx index 5938ef2..5efa676 100644 --- a/app/components/reviews/FileInfo.tsx +++ b/app/components/reviews/FileInfo.tsx @@ -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'; diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index f73c323..8476ac1 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -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(null); const [pageInputValue, setPageInputValue] = useState(''); + // 拖拽状态管理 + 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) => { + 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) => { + 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(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 {/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
{/* 渲染PDF页面组件 */} @@ -305,7 +356,14 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointResult // 渲染文档内容 const renderDocumentContent = () => { return ( -
+
100 ? `${zoomLevel}%` : '100%', + overflow: 'visible' + }} + > PDF文档加载失败,请检查链接或网络连接。
} noData={
无数据
} loading={
PDF加载中...
} @@ -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} > - + 返回顶部 */} {"比例:"+zoomLevel+"%"} +
- {loadError ? ( -
-

{loadError}

-
- ) : ( - renderDocumentContent() - )} +
); diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index a6f9e00..e589d42 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -559,53 +559,6 @@ export function ReviewPointsList({ {/* 评查点内容显示区域 */} {reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
- {/* 修改评查结果的结构之前,先显示旧的结构 */} - {/* {Object.entries(reviewPoint.content).map(([key, value], index) => ( -
{ - // 阻止事件冒泡,防止触发父元素的点击事件 - 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}内容详情`} - > -
- {key} - - {value ? '' : '缺失'} - -
-

- {typeof value === 'object' && value !== null - ? (value.value || (value.value === '' ? 占位符 : '')) - : (value || (value === '' ? 占位符 : ''))} -

-
- ))} */} {/* 修改评查结果的结构之后,显示新的结构 */} {renderContent(reviewPoint)}
@@ -662,54 +615,6 @@ export function ReviewPointsList({ )}
- {/* 修改评查结果的结构之前,先显示旧的结构 */} - {/* {Object.entries(reviewPoint.content).map(([key, value], index) => ( -
{ - // 阻止事件冒泡,防止触发父元素的点击事件 - 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}内容详情`} - > - -
- {key} - - {value ? '' : '缺失'} - -
-

- {typeof value === 'object' && value !== null - ? (value.value || (value.value === '' ? 占位符 : '')) - : (value || (value === '' ? 占位符 : ''))} -

-
- ))} */} {/* 修改评查结果的结构之后,显示新的结构 */} {renderContent(reviewPoint)}
@@ -777,56 +682,6 @@ export function ReviewPointsList({ {/* 内容显示区域 */}
- {/* 修改评查结果的结构之前,先显示旧的结构 */} - {/* {Object.entries(reviewPoint.content).map(([key, value], index) => ( -
{ - // 阻止事件冒泡,防止触发父元素的点击事件 - 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}内容详情`} - > -
- {key} - - {value ? '' : '缺失'} - -
-

- {typeof value === 'object' && value !== null - ? (value.value || (value.value === '' ? 占位符 : '')) - : (value || (value === '' ? 占位符 : ''))} -

-
- ))} */} {/* 修改评查结果的结构之后,显示新的结构 */} {renderContent(reviewPoint)}
diff --git a/app/components/ui/RouteChangeLoader.tsx b/app/components/ui/RouteChangeLoader.tsx new file mode 100644 index 0000000..4e30426 --- /dev/null +++ b/app/components/ui/RouteChangeLoader.tsx @@ -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; \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 3f0b239..737af90 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -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() { + diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index a160e35..c4dd58f 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -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(); } // 导航到评查详情页 diff --git a/app/routes/examples/pdfview.tsx b/app/routes/examples/pdfview.tsx new file mode 100644 index 0000000..5f02fdd --- /dev/null +++ b/app/routes/examples/pdfview.tsx @@ -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(null); + const [numPages, setNumPages] = useState(0); + const [pageNumber, setPageNumber] = useState(1); + const [pageWidth, setPageWidth] = useState(defaultWidth); + const [fullscreen, setFullscreen] = useState(false); + const [rotation, setRotation] = useState(0); + const [showThumbnails, setShowThumbnails] = useState(false); + const [visiblePages, setVisiblePages] = useState([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=()=>(
+
+
+
+ + +
+ } + loading={
} + > + {showThumbnails ? ( +
+ {Array.from({ length: numPages }, (_, i) => i + 1).map((page) => ( +
{ + setPageNumber(page); + setShowThumbnails(false); + }} + > + {visiblePages.includes(page) ? ( + } + renderTextLayer={false} // 禁用文本层,提升性能 + renderAnnotationLayer={false} // 禁用注释层 + /> + ) : ( +
第 {page} 页
+ )} + 第 {page} 页 +
+ ))} +
+ ) : ( + } + renderTextLayer={false} // 禁用文本层 + renderAnnotationLayer={false} // 禁用注释层 + error={() => setPageNumber(1)} + /> + )} + +
+
+
+
+ + + + {' '} + / {numPages} + + + + + + + + + + + + + + + + + + + + {fullscreen ? : } + + {onClose && ( + + + + )} +
+
+
+
) + if(parentDom){ + return renderContent() + } + return createPortal( + renderContent(), + parent,) +}; + +export default PDFView; \ No newline at end of file diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 6791bc3..abfad49 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -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: '错误',