From ed3ff4c3b39b16dce7f44b25655072d084601e6a Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Sat, 24 May 2025 23:25:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8B=E8=BD=BD,=E6=9B=B4?= =?UTF-8?q?=E6=94=B9logo,=E4=BC=98=E5=8C=96=E8=AF=84=E6=9F=A5=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=86=85=E5=AE=B9=E7=9A=84=E6=98=BE=E7=A4=BA,?= =?UTF-8?q?=E4=BF=AE=E6=94=B9sidebar=E7=9A=84=E9=A6=96=E9=A1=B5,=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=90=88=E5=90=8C?= =?UTF-8?q?=E7=9A=84=E5=BC=82=E6=AD=A5=E4=B8=8A=E4=BC=A0=E6=97=B6=E5=BA=8F?= =?UTF-8?q?=E9=97=AE=E9=A2=98,=E9=A6=96=E9=A1=B5=E6=9C=80=E8=BF=91?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=9A=84=E8=87=AA=E5=8A=A8=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/files/files-upload.ts | 92 ++- app/components/layout/Sidebar.tsx | 8 +- app/components/reviews/FileInfo.tsx | 3 +- app/components/reviews/FilePreview.tsx | 22 +- app/components/reviews/ReviewPointsList.tsx | 33 +- app/routes/_index.tsx | 63 +- app/routes/documents.edit.tsx | 75 +- app/routes/files.upload.tsx | 738 +++++++++++++++----- app/routes/reviews.tsx | 3 +- public/logo.svg | 1 + 10 files changed, 801 insertions(+), 237 deletions(-) create mode 100644 public/logo.svg diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index b227f98..bae14e0 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -146,12 +146,15 @@ export async function uploadDocumentToServer( isTestDocument: boolean = false ): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> { try { + console.log('【调试】开始上传文档:', { fileName, fileSize: binaryData.byteLength }); + // 创建FormData对象 const formData = new FormData(); // 将二进制数据转换为Blob并添加到FormData const blob = new Blob([binaryData], { type: fileType }); formData.append('file', blob, fileName); + console.log('【调试】Blob已创建,文件大小:', blob.size); // 将信息添加到一个JSON对象中 const uploadInfo = { @@ -164,48 +167,67 @@ export async function uploadDocumentToServer( // 添加JSON字符串到FormData formData.append('upload_info', JSON.stringify(uploadInfo)); + console.log('【调试】FormData准备完成:', JSON.stringify(uploadInfo)); - console.log('上传信息:', { - fileName, - fileType, - typeId: Number(typeId), - priority, - documentNumber: documentNumber || null, - remark: remark || null, - isTestDocument - }); + console.log('【调试】准备发送请求到服务器:', 'http://172.16.0.58:8008/admin/documents/upload'); // 发送请求 // const response = await fetch(`${API_BASE_URL}/admin/documents/upload`, { - const response = await fetch('http://172.16.0.58:8008/admin/documents/upload', { - // const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', { - // const response = await fetch('http://172.16.0.119:8000/admin/documents/upload', { - method: 'POST', - headers: { - 'X-File-Name': encodeURIComponent(fileName) - }, - body: formData - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`上传失败 (${response.status}): ${errorText}`); - return { - error: `上传失败: ${response.status} ${response.statusText}`, - status: response.status + try { + console.log('【调试】开始fetch请求...'); + const response = await fetch('http://172.16.0.58:8008/admin/documents/upload', { + // const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', { + // const response = await fetch('http://172.16.0.119:8000/admin/documents/upload', { + method: 'POST', + headers: { + 'X-File-Name': encodeURIComponent(fileName) + }, + body: formData + }); + + console.log('【调试】收到服务器响应:', { status: response.status, statusText: response.statusText }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`【调试】上传失败 (${response.status}): ${errorText}`); + return { + error: `上传失败: ${response.status} ${response.statusText} - ${errorText}`, + status: response.status + }; + } + + console.log('【调试】开始解析JSON响应'); + let responseData; + try { + responseData = await response.json(); + console.log('【调试】JSON响应解析成功:', responseData); + } catch (jsonError) { + console.error('【调试】JSON解析失败:', jsonError); + return { + error: `解析响应JSON失败: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`, + status: 500 + }; + } + + const extractedData = extractApiData(responseData); + console.log('【调试】提取的数据:', extractedData); + + if (!extractedData) { + console.error('【调试】无法提取数据'); + return { error: '处理上传响应失败', status: 500 }; + } + + console.log('【调试】上传成功,返回数据'); + return { data: extractedData }; + } catch (fetchError) { + console.error('【调试】fetch请求失败:', fetchError); + return { + error: `fetch请求错误: ${fetchError instanceof Error ? fetchError.message : '未知错误'}`, + status: 500 }; } - - const responseData = await response.json(); - const extractedData = extractApiData(responseData); - - if (!extractedData) { - return { error: '处理上传响应失败', status: 500 }; - } - - return { data: extractedData }; } catch (error) { - console.error('上传错误:', error); + console.error('【调试】上传过程中发生错误:', error); return { error: error instanceof Error ? error.message : '上传失败', status: 500 diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 2c0e51b..2e58a84 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -21,7 +21,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { const menuItems: MenuItem[] = [ { id: 'home', - title: '首页', + title: '系统概览', path: '/', icon: 'ri-home-line' }, @@ -164,7 +164,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
- + 智慧法务 {!collapsed &&

智慧法务

}
- {!collapsed && ( + {/* {!collapsed && (
@@ -187,7 +187,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {

超级管理员

- )} + )} */}
{menuItems.map((item) => ( diff --git a/app/components/reviews/FileInfo.tsx b/app/components/reviews/FileInfo.tsx index bb52907..4cb9f30 100644 --- a/app/components/reviews/FileInfo.tsx +++ b/app/components/reviews/FileInfo.tsx @@ -30,7 +30,8 @@ export function FileInfo({ fileInfo }: FileInfoProps) { {fileInfo.fileName} - 合同编号:{fileInfo.contractNumber} + {/* 合同编号:{fileInfo.contractNumber} */} + 卷宗编号:{fileInfo.contractNumber} {fileInfo.fileSize && ( diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index 50c1361..d6a86f1 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -180,22 +180,22 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage // 如果有目标页码,并且与上次不同或activeReviewPointId变化了,则执行跳转 if (targetPage && numPages && targetPage <= numPages && (targetPage !== prevTargetPageRef.current || activeReviewPointResultId)) { prevTargetPageRef.current = targetPage; - const newTargetPage = targetPage; + let newTargetPage = targetPage; // let newTargetPage = targetPage; // console.log("targetPage:", targetPage); // console.log("fileContent:", fileContent); // 页码偏移量 - // try { - // // 安全地访问ocrResult - // if (fileContent.ocrResult && fileContent.ocrResult.__meta && fileContent.ocrResult.__meta.page_offset) { - // // 可以根据需要使用page_offset调整目标页面 - // newTargetPage = targetPage + fileContent.ocrResult.__meta.page_offset; - // } - // } catch (error) { - // console.error("访问ocrResult时出错:", error); - // toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误')); - // } + try { + // 安全地访问ocrResult + if (fileContent.ocrResult && fileContent.ocrResult.__meta && fileContent.ocrResult.__meta.page_offset) { + // 可以根据需要使用page_offset调整目标页面 + newTargetPage = targetPage + fileContent.ocrResult.__meta.page_offset; + } + } catch (error) { + console.error("访问ocrResult时出错:", error); + toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误')); + } const pageElement = document.getElementById(`page-${newTargetPage}`); if (pageElement) { diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 97d58f1..5650e00 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -450,7 +450,7 @@ export function ReviewPointsList({ {Object.entries(reviewPoint.content).map(([key, value], index) => (
{ e.stopPropagation(); console.log(`单独点击${key}----`, reviewPoint); @@ -488,8 +488,17 @@ export function ReviewPointsList({ role="button" tabIndex={0} aria-label={`查看${key}内容详情`} + onMouseLeave={(e) => { + // 获取容器内的滚动区域元素 + const scrollContainer = e.currentTarget.querySelector('.text-container'); + if (scrollContainer) { + // 在文本缩回之前重置滚动位置 + scrollContainer.scrollTop = 0; + } + }} > -
+ {/*
*/} +
{key} @@ -498,11 +507,18 @@ export function ReviewPointsList({ {value.value?.toString().trim() ? '' : '缺失'}
-

- {(value.value?.toString().trim() === '') - ? 占位符 - : value.value?.toString() || ''} -

+
+

+ {(value.value?.toString().trim() === '') + ? "" + : value.value?.toString() || ''} +

+
))} @@ -897,7 +913,8 @@ export function ReviewPointsList({
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 82598fa..24be06a 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -76,7 +76,9 @@ export async function loader() { } export default function Index() { - const { homeData, recentFiles } = useLoaderData(); + const { homeData, recentFiles: initialRecentFiles } = useLoaderData(); + // 使用useState存储最近文档数据,初始值为loader加载的数据 + const [recentFiles, setRecentFiles] = useState(initialRecentFiles || []); const [currentDateTime, setCurrentDateTime] = useState({ date: '', time: '' @@ -102,16 +104,71 @@ export default function Index() { // 清理函数,组件卸载时清除计时器 return () => clearInterval(timerID); }, []); - + + // 修改useEffect定时器,每10秒自动获取最近文档数据 + // 按照定时器更新最近文档 + useEffect(() => { + // 定义一个函数用于获取最新的文档数据 + const fetchLatestDocuments = async () => { + try { + const documentSearchParams = { + page: 1, + pageSize: 10, + order: 'updated_at.desc' + }; + + console.log('定时获取最新文档数据...'); + const responseDocuments = await getDocuments(documentSearchParams); + + if (responseDocuments.error) { + console.error('获取最近文档数据失败', responseDocuments.error); + return; + } + + // 获取新的文档数据 + const newRecentFiles = responseDocuments.data?.documents || []; + + // 检查数据是否有变化 + if (JSON.stringify(newRecentFiles) !== JSON.stringify(recentFiles)) { + console.log('文档数据已更新,直接更新状态'); + // 直接更新状态,不需要刷新页面 + setRecentFiles(newRecentFiles); + } + } catch (error) { + console.error('自动获取文档数据失败:', error); + } + }; + + // 设置10秒的定时器 + const timerID = setInterval(fetchLatestDocuments, 10000); + + // 组件卸载时清除定时器 + return () => { + console.log('清除文档数据自动更新定时器'); + clearInterval(timerID); + }; + }, []); // 不再依赖recentFiles,避免循环依赖 + return (
{/* 页面头部 */}

系统概览

-
+
+
{currentDateTime.date} | {currentDateTime.time} +
+
+
+ +
+
+

系统管理员

+

超级管理员

+
+
diff --git a/app/routes/documents.edit.tsx b/app/routes/documents.edit.tsx index c7e7250..0b85799 100644 --- a/app/routes/documents.edit.tsx +++ b/app/routes/documents.edit.tsx @@ -181,7 +181,7 @@ export async function action({ request }: ActionFunctionArgs) { // 文档编辑页面组件 export default function DocumentEdit() { - const { document, documentTypes } = useLoaderData(); + const { document: documentData, documentTypes } = useLoaderData(); const actionData = useActionData(); const navigate = useNavigate(); const [numPages, setNumPages] = useState(0); @@ -190,10 +190,10 @@ export default function DocumentEdit() { // 表单状态管理 - 使用受控组件 const [formValues, setFormValues] = useState({ - documentNumber: document.documentNumber || "", - auditStatus: document.auditStatus, - isTest: document.isTest || false, - remark: document.remark || "" + documentNumber: documentData.documentNumber || "", + auditStatus: documentData.auditStatus, + isTest: documentData.isTest || false, + remark: documentData.remark || "" }); // 表单验证错误状态 @@ -303,7 +303,7 @@ export default function DocumentEdit() { return (
{ console.error("PDF加载错误:", error); @@ -393,9 +393,49 @@ export default function DocumentEdit() { ); }; + // 下载文档 + const downloadDocument = async () => { + try { + const downloadUrl = `${DOCUMENT_URL}${documentData.path}`; + + // 使用fetch获取文件内容 + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error(`下载失败: ${response.status} ${response.statusText}`); + } + + // 将响应转换为Blob + const blob = await response.blob(); + + // 创建Blob URL + const blobUrl = URL.createObjectURL(blob); + + // 创建一个隐藏的a标签并点击它 + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = blobUrl; + // 从路径中获取文件名 + const fileName = documentData.path.split('/').pop() || documentData.name; + a.download = decodeURIComponent(fileName); + document.body.appendChild(a); + a.click(); + + // 清理 + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(blobUrl); + }, 100); + + toastService.success('文件下载成功'); + } catch (error) { + console.error('下载文件失败:', error); + toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); + } + }; + // 在新窗口打开文档预览 const openPreview = () => { - const previewUrl = `${DOCUMENT_URL}${document.path}`; + const previewUrl = `${DOCUMENT_URL}${documentData.path}`; window.open(previewUrl, '_blank'); }; @@ -428,7 +468,7 @@ export default function DocumentEdit() {
-
{document.name}
+
{documentData.name}
- {getDocumentTypeName(document.type)} + {getDocumentTypeName(documentData.type)}
- {document.uploadTime} + {documentData.uploadTime}
- {formatFileSize(document.size)} + {formatFileSize(documentData.size)}
- {renderStatusBadge(document.auditStatus)} + {renderStatusBadge(documentData.auditStatus)}
@@ -477,7 +517,7 @@ export default function DocumentEdit() { id="type-id" name="type_id" className="form-select" - value={document.type} + value={documentData.type} disabled={true} required > @@ -566,8 +606,8 @@ export default function DocumentEdit() {
- - {document.name} + + {documentData.name}
@@ -607,7 +648,7 @@ export default function DocumentEdit() { time: "2023-10-15 15:30", user: "系统", action: "创建了此文档", - details: `首次上传文档,文档类型:${getDocumentTypeName(document.type)},状态:待审核` + details: `首次上传文档,文档类型:${getDocumentTypeName(documentData.type)},状态:待审核` }, { time: "2023-10-15 16:45", diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 02c6e6d..3957646 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { MetaFunction, ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; -import { Form, useActionData, useLoaderData, useNavigate } from "@remix-run/react"; +import { Form, useActionData, useLoaderData, useNavigate, useBlocker } from "@remix-run/react"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; import { Table } from "~/components/ui/Table"; @@ -145,11 +145,13 @@ async function uploadFileToServer( } // 确保返回有效的FileUploadResponse对象 + // console.log('上传成功:', response.data); if (response.data) { return response.data; } // 如果没有数据,则返回错误 + // console.log('上传失败:', response.error); return { success: false, error: '上传失败,未获取到响应数据' @@ -349,10 +351,16 @@ export default function FilesUpload() { // 状态检查定时器引用 const statusCheckIntervalRef = useRef(null); + // 添加组件挂载状态引用 + const isMountedRef = useRef(true); + // useEffect 处理上传队列状态检查定时器 - 只在组件卸载时清除 useEffect(() => { console.log('设置上传队列状态检查定时器'); + // 标记组件已挂载 + isMountedRef.current = true; + // 设置定时器检查队列中文件的状态,初始先加载一次查询 checkQueueStatus(); statusCheckIntervalRef.current = setInterval(checkQueueStatus, 10000); @@ -360,6 +368,8 @@ export default function FilesUpload() { // 只在组件卸载时清除 return () => { console.log('组件卸载,清除上传队列状态检查定时器'); + // 标记组件已卸载 + isMountedRef.current = false; if (statusCheckIntervalRef.current) { clearInterval(statusCheckIntervalRef.current); statusCheckIntervalRef.current = null; @@ -446,24 +456,37 @@ export default function FilesUpload() { const value = e.target.value; // 确保只有选择了有效的文件类型才进行设置 if (value) { + console.log('【调试-handleFileTypeChange】文件类型变更为:', value); setFileType(value as FileType); // 立即清除错误状态 setFileTypeError(null); // 检查是否选择了合同类型 - // const selectedType = documentTypes.find(t => t.id.toString() === value); - // const isContract = !!(selectedType && selectedType.name.includes('合同')); - // setIsContractType(isContract); + const selectedType = documentTypes.find(t => t.id.toString() === value); + const isContract = !!(selectedType && selectedType.name.includes('合同')); + console.log('【调试-handleFileTypeChange】文件类型检查:', { + selectedType, + isContract, + typeName: selectedType?.name, + currentFiles: currentFiles.length + }); + + setIsContractType(isContract); // 重置文件状态 setContractMainFiles([]); setContractAttachmentFiles([]); setCurrentFiles([]); - // 如果已经有选中的文件,且选择了文件类型,则开始上传 - // if (currentFiles.length > 0 && !isContract) { - // startUpload(currentFiles); - // } + // 如果已经有选中的文件,且选择了文件类型,且不是合同类型,则开始上传 + if (currentFiles.length > 0 && !isContract) { + console.log('【调试-handleFileTypeChange】自动开始上传非合同类型文件'); + startUpload(currentFiles); + } else if (currentFiles.length > 0 && isContract) { + console.log('【调试-handleFileTypeChange】合同类型需要手动点击开始上传按钮'); + // 合同类型不自动上传,需要用户先上传主文件和附件,然后点击开始上传按钮 + setCurrentFiles([]); + } } else { setFileType(""); @@ -475,91 +498,248 @@ export default function FilesUpload() { // 处理合同主文件选择 const handleContractMainFilesSelected = (files: FileList) => { - if (files.length > 0) { - // 验证文件类型,只允许PDF文件 - const validFiles: File[] = []; - let hasInvalidFiles = false; + try { + console.log('【调试-handleContractMainFilesSelected】开始处理合同主文件选择, 文件数量:', files.length); - Array.from(files).forEach(file => { - if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { - validFiles.push(file); - } else { - hasInvalidFiles = true; - } - }); + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-handleContractMainFilesSelected】组件已卸载,取消处理'); + return; + } - if (hasInvalidFiles) { - // 显示错误提示 - messageService.error('只能上传PDF格式的文件', { - title: '文件类型错误', - confirmText: '确定', - cancelText: '', + if (files.length > 0) { + // 验证文件类型,只允许PDF文件 + const validFiles: File[] = []; + let hasInvalidFiles = false; + + Array.from(files).forEach(file => { + if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { + validFiles.push(file); + } else { + hasInvalidFiles = true; + console.error(`【调试-handleContractMainFilesSelected】无效的文件类型: ${file.name}, 类型: ${file.type}`); + } }); + + if (hasInvalidFiles) { + // 显示错误提示 + console.error('【调试-handleContractMainFilesSelected】存在无效的文件类型'); + messageService.error('只能上传PDF格式的文件', { + title: '文件类型错误', + confirmText: '确定', + cancelText: '', + }); + } + + if (validFiles.length > 0 && isMountedRef.current) { + console.log('【调试-handleContractMainFilesSelected】有效文件数量:', validFiles.length); + console.log('【调试-handleContractMainFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); + + setContractMainFiles(validFiles); + } else { + console.error('【调试-handleContractMainFilesSelected】没有有效的PDF文件或组件已卸载'); + } + } else { + console.log('【调试-handleContractMainFilesSelected】未选择任何文件'); } - - if (validFiles.length > 0) { - setContractMainFiles(validFiles); - checkAndPrepareUpload(validFiles, contractAttachmentFiles); - } + } catch (error) { + console.error('【调试-handleContractMainFilesSelected】处理合同主文件选择时发生错误:', error); } }; // 处理合同附件选择 const handleContractAttachmentFilesSelected = (files: FileList) => { - if (files.length > 0) { - // 验证文件类型,只允许PDF文件 - const validFiles: File[] = []; - let hasInvalidFiles = false; + try { + console.log('【调试-handleContractAttachmentFilesSelected】开始处理合同附件选择, 文件数量:', files.length); - Array.from(files).forEach(file => { - if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { - validFiles.push(file); - } else { - hasInvalidFiles = true; - } - }); + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-handleContractAttachmentFilesSelected】组件已卸载,取消处理'); + return; + } - if (hasInvalidFiles) { - // 显示错误提示 - messageService.error('只能上传PDF格式的文件', { - title: '文件类型错误', - confirmText: '确定', - cancelText: '', + if (files.length > 0) { + // 验证文件类型,只允许PDF文件 + const validFiles: File[] = []; + let hasInvalidFiles = false; + + Array.from(files).forEach(file => { + if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { + validFiles.push(file); + } else { + hasInvalidFiles = true; + console.error(`【调试-handleContractAttachmentFilesSelected】无效的文件类型: ${file.name}, 类型: ${file.type}`); + } }); + + if (hasInvalidFiles) { + // 显示错误提示 + console.error('【调试-handleContractAttachmentFilesSelected】存在无效的文件类型'); + messageService.error('只能上传PDF格式的文件', { + title: '文件类型错误', + confirmText: '确定', + cancelText: '', + }); + } + + if (validFiles.length > 0 && isMountedRef.current) { + console.log('【调试-handleContractAttachmentFilesSelected】有效文件数量:', validFiles.length); + console.log('【调试-handleContractAttachmentFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); + + setContractAttachmentFiles(validFiles); + } else { + console.error('【调试-handleContractAttachmentFilesSelected】没有有效的PDF文件或组件已卸载'); + } + } else { + console.log('【调试-handleContractAttachmentFilesSelected】未选择任何文件'); } - - if (validFiles.length > 0) { - setContractAttachmentFiles(validFiles); - checkAndPrepareUpload(contractMainFiles, validFiles); - } + } catch (error) { + console.error('【调试-handleContractAttachmentFilesSelected】处理合同附件选择时发生错误:', error); } }; // 检查并准备上传 const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => { - // 当两个区域都有文件时才准备上传 - if (mainFiles.length > 0 && attachmentFiles.length > 0) { - // 合并所有文件 - const allFiles = [...mainFiles, ...attachmentFiles]; - - // 这里的currentFiles的长度是上传进度条是否显示的关键 - // setCurrentFiles(allFiles); + try { + console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', { + mainFilesCount: mainFiles.length, + attachmentFilesCount: attachmentFiles.length, + fileType + }); - // 将准备上传的操作移到这里,暂时不执行 - console.log('准备上传', allFiles.length, '个文件'); - // startUpload(allFiles); + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-checkAndPrepareUpload】组件已卸载,取消操作'); + return; + } + + // 检查是否选择了文件类型 + if (!fileType) { + console.error('【调试-checkAndPrepareUpload】未选择文件类型'); + toastService.error('请先选择文件类型'); + return; + } + + // 检查是否为合同类型 + const selectedType = documentTypes.find(t => t.id.toString() === fileType); + const isContract = !!(selectedType && selectedType.name.includes('合同')); + + console.log('【调试-checkAndPrepareUpload】文件类型检查', { + selectedType, + isContract, + typeName: selectedType?.name + }); + + if (isContract) { + console.log('【调试-checkAndPrepareUpload】合同文档类型特殊处理'); + + // 检查主文件 + if(mainFiles.length === 0) { + console.error('【调试-checkAndPrepareUpload】缺少合同主文件'); + toastService.error('请上传合同主文件'); + return; + } + + // 记录主文件和附件文件信息 + console.log('【调试-checkAndPrepareUpload】合同主文件:', mainFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); + if (attachmentFiles.length > 0) { + console.log('【调试-checkAndPrepareUpload】合同附件文件:', attachmentFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); + } else { + console.log('【调试-checkAndPrepareUpload】无合同附件文件'); + } + } + + if (mainFiles.length > 0) { + // 合并所有文件 + let allFiles = [...mainFiles]; + + // 如果附件文件存在,则合并 + if (attachmentFiles.length > 0) { + allFiles = [...allFiles, ...attachmentFiles]; + } + + console.log('【调试-checkAndPrepareUpload】合并文件后总数:', allFiles.length); + + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-checkAndPrepareUpload】组件已卸载,取消操作'); + return; + } + + // 这里的currentFiles的长度是上传进度条是否显示的关键 + setCurrentFiles(allFiles); + + // 将准备上传的操作移到这里,暂时不执行 + console.log('【调试-checkAndPrepareUpload】准备上传', allFiles.length, '个文件'); + + if (fileType) { + try { + // 再次检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-checkAndPrepareUpload】组件已卸载,取消上传'); + return; + } + + console.log('【调试-checkAndPrepareUpload】开始调用startUpload函数'); + + // 使用 setTimeout 延迟调用,确保状态已更新 + setTimeout(() => { + if (isMountedRef.current) { + try { + startUpload(allFiles); + } catch (delayedError) { + console.error('【调试-checkAndPrepareUpload】延迟调用startUpload失败:', delayedError); + toastService.error(`准备上传文件失败: ${delayedError instanceof Error ? delayedError.message : '未知错误'}`); + } + } else { + console.error('【调试-checkAndPrepareUpload】组件已卸载,取消延迟上传'); + } + }, 0); + + } catch (uploadError) { + console.error('【调试-checkAndPrepareUpload】调用startUpload失败:', uploadError); + + // 检查组件是否已卸载 + if (isMountedRef.current) { + toastService.error(`准备上传文件失败: ${uploadError instanceof Error ? uploadError.message : '未知错误'}`); + } + } + } else { + console.error('【调试-checkAndPrepareUpload】未选择文件类型,无法上传'); + toastService.error('请选择文件类型'); + } + } else { + console.error('【调试-checkAndPrepareUpload】没有文件可上传'); + toastService.error('请选择要上传的文件'); + } + } catch (error) { + console.error('【调试-checkAndPrepareUpload】准备上传文件过程中发生错误:', error); + + // 检查组件是否已卸载 + if (isMountedRef.current) { + toastService.error(`准备上传文件失败: ${error instanceof Error ? error.message : '未知错误'}`); + } } }; // 开始上传文件 const startUpload = async (files: File[]) => { try { + console.log('【调试-startUpload】开始上传过程,文件数量:', files.length); + + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-startUpload】组件已卸载,取消上传'); + return; + } + // 再次验证所有文件类型,确保只有PDF文件 const invalidFiles = files.filter(file => file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf') ); if (invalidFiles.length > 0) { + console.error('【调试-startUpload】文件类型验证失败:', invalidFiles.map(f => f.name)); throw new Error('只能上传PDF格式的文件'); } @@ -570,14 +750,23 @@ export default function FilesUpload() { const totalSize = files.reduce((sum, file) => sum + file.size, 0); let uploadedSize = 0; + console.log('【调试-startUpload】总文件大小:', formatFileSize(totalSize)); + // 更新步骤状态 const updatedSteps = [...processingSteps]; updatedSteps[0].status = "active"; updatedSteps[0].description = `正在上传 ${files.length} 个文件到服务器...`; - setProcessingSteps(updatedSteps); + + // 检查组件是否已卸载 + if (isMountedRef.current) { + setProcessingSteps(updatedSteps); + } else { + console.log('【调试-startUpload】组件已卸载,不更新处理步骤'); + return; + } // 转换文件为二进制格式 - console.log("开始转换文件到二进制格式..."); + console.log("【调试-startUpload】开始转换文件到二进制格式..."); // 模拟上传进度 if (progressIntervalRef.current) { @@ -605,45 +794,103 @@ export default function FilesUpload() { const uploadedFiles: UploadedFile[] = []; for (const file of files) { - // 转换文件为二进制格式 - const binaryData = await uploadFileToBinary(file); - - // 上传文件 - const response = await uploadFileToServer( - binaryData, - file.name, - file.type, - fileType as FileType, - priority, - documentNumber || null, - remark || null, - isTestDocument - ); - - if (!response.success || !response.result) { - throw new Error(response.error || "上传失败"); - } - - // 更新已上传大小 - uploadedSize += file.size; - - // 创建新的文件对象 - const newFile: UploadedFile = { - id: response.result.id, - name: response.result.file_name, - size: response.result.file_size, - type: file.type, - fileType: fileType as FileType, - priority, - status: DocumentStatus.WAITING, - uploadTime: getCurrentTime(), - processingInfo: { - progress: 0, - currentStep: 0 + try { + console.log(`【调试-startUpload】准备上传文件: ${file.name}, 大小: ${formatFileSize(file.size)}`); + + // 转换文件为二进制格式 + console.log(`【调试-startUpload】开始转换文件 ${file.name} 为二进制格式`); + let binaryData: ArrayBuffer; + try { + binaryData = await uploadFileToBinary(file); + console.log(`【调试-startUpload】文件 ${file.name} 二进制转换成功,大小: ${binaryData.byteLength} 字节`); + } catch (binaryError) { + console.error(`【调试-startUpload】文件 ${file.name} 二进制转换失败:`, binaryError); + throw new Error(`文件 ${file.name} 转换失败: ${binaryError instanceof Error ? binaryError.message : '未知错误'}`); } - }; - - uploadedFiles.push(newFile); + + let response: FileUploadResponse; + console.log(`【调试-startUpload】开始上传文件 ${file.name} 到服务器,文件类型: ${fileType}`); + + try { + // 上传文件 + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-startUpload】组件已卸载,取消上传'); + return { success: false, error: '组件已卸载' } as FileUploadResponse; + } + + console.log(`【调试-startUpload】准备上传文件 ${file.name} 到服务器`); + + // 使用Promise.race添加超时处理 + const uploadPromise = uploadFileToServer( + binaryData, + file.name, + file.type, + fileType as FileType, + priority, + documentNumber || null, + remark || null, + isTestDocument + ); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('上传超时')); + }, 30000); // 30秒超时 + }); + + // 使用Promise.race处理超时 + response = await Promise.race([uploadPromise, timeoutPromise]); + + // 再次检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-startUpload】组件已卸载,忽略上传响应'); + return; + } + + console.log(`【调试-startUpload】文件 ${file.name} 上传响应:`, response); + } catch (error) { + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-startUpload】组件已卸载,忽略上传错误'); + return; + } + + console.error(`【调试-startUpload】文件 ${file.name} 上传错误:`, error); + throw new Error(`文件 ${file.name} 上传失败: ${error instanceof Error ? error.message : '未知错误'}`); + } + + if (!response.success || !response.result) { + console.error(`【调试-startUpload】文件 ${file.name} 上传失败:`, response.error); + throw new Error(response.error || `文件 ${file.name} 上传失败`); + } + + // 更新已上传大小 + uploadedSize += file.size; + + // 创建新的文件对象 + const newFile: UploadedFile = { + id: response.result.id, + name: response.result.file_name, + size: response.result.file_size, + type: file.type, + fileType: fileType as FileType, + priority, + status: DocumentStatus.WAITING, + uploadTime: getCurrentTime(), + processingInfo: { + progress: 0, + currentStep: 0 + } + }; + + console.log(`【调试-startUpload】文件 ${file.name} 上传成功,文件ID: ${newFile.id}`); + uploadedFiles.push(newFile); + } catch (fileError) { + console.error(`【调试-startUpload】处理文件 ${file.name} 时发生错误:`, fileError); + // 继续抛出错误,让外层catch捕获 + throw fileError; + } } // 清除进度定时器 @@ -669,15 +916,17 @@ export default function FilesUpload() { }; }); + console.log(`【调试-startUpload】所有文件上传完成,更新队列`); setQueueFiles(prev => [...newDocuments, ...prev]); // 设置当前文件为已上传的文件 setCompletedFiles(uploadedFiles); // 完成上传后开始处理流程 + console.log(`【调试-startUpload】开始文件处理流程`); startProcessing(uploadedFiles); } catch (error) { - console.error("文件上传错误:", error); + console.error("【调试-startUpload】文件上传过程发生错误:", error); // 更新步骤状态为错误 const errorSteps = [...processingSteps]; @@ -701,81 +950,149 @@ export default function FilesUpload() { } }); resetUpload(); + + // 抛出错误,让React错误边界捕获并显示 + throw error; } }; // 开始处理上传的文件 const startProcessing = (files: UploadedFile[]) => { - // 更新上传阶段 - setUploadStage("processing"); - - // 更新步骤状态 - const updatedSteps = processingSteps.map(step => ({...step})); - updatedSteps[0].status = "done"; - updatedSteps[0].description = "文件已成功上传到服务器"; - updatedSteps[1].status = "active"; - updatedSteps[1].description = "正在转换文档格式,拆分文档内容..."; - - setProcessingSteps(updatedSteps); - - // 获取文件ID列表 - const fileIds = files.map(file => file.id).filter(id => id > 0); - - console.log('开始处理文件,设置文件处理进度定时器'); - - // 清除之前的进度定时器(如果存在) - if (progressIntervalRef.current) { - clearInterval(progressIntervalRef.current); + try { + console.log('【调试-startProcessing】开始处理上传的文件:', files.length, '个文件'); + + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-startProcessing】组件已卸载,取消处理'); + return; + } + + // 更新上传阶段 + setUploadStage("processing"); + + // 更新步骤状态 + const updatedSteps = processingSteps.map(step => ({...step})); + updatedSteps[0].status = "done"; + updatedSteps[0].description = "文件已成功上传到服务器"; + updatedSteps[1].status = "active"; + updatedSteps[1].description = "正在转换文档格式,拆分文档内容..."; + + setProcessingSteps(updatedSteps); + + // 获取文件ID列表 + const fileIds = files.map(file => file.id).filter(id => id > 0); + console.log('【调试-startProcessing】文件ID列表:', fileIds); + + if (fileIds.length === 0) { + console.error('【调试-startProcessing】没有有效的文件ID,无法开始处理'); + throw new Error('没有有效的文件ID,无法开始处理'); + } + + console.log('【调试-startProcessing】开始处理文件,设置文件处理进度定时器'); + + // 清除之前的进度定时器(如果存在) + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + + // 立即开始检查状态 + try { + console.log('【调试-startProcessing】立即开始检查处理状态'); + checkProcessingStatus(fileIds); + } catch (statusError) { + console.error('【调试-startProcessing】首次检查状态失败:', statusError); + } + + // 设置文件处理进度定时器,每10秒检查一次状态 + progressIntervalRef.current = setInterval(() => { + console.log('【调试-startProcessing】文件处理进度定时器触发,检查文件状态'); + try { + checkProcessingStatus(fileIds); + } catch (intervalError) { + console.error('【调试-startProcessing】定时检查状态失败:', intervalError); + // 不要抛出,继续尝试 + } + }, 10000); + } catch (error) { + console.error('【调试-startProcessing】处理文件过程中发生错误:', error); + + // 清除进度定时器 + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + progressIntervalRef.current = null; + } + + // 更新步骤状态为错误 + const errorSteps = [...processingSteps]; + for (let i = 0; i < errorSteps.length; i++) { + if (errorSteps[i].status === "active") { + errorSteps[i].status = "error"; + errorSteps[i].description = `处理失败: ${error instanceof Error ? error.message : '未知错误'}`; + } + } + setProcessingSteps(errorSteps); + + // 重置处理状态 + setUploadStage("idle"); + + // 显示错误消息 + messageService.error(`文件处理失败:${error instanceof Error ? error.message : '未知错误'}`, { + title: '处理失败', + confirmText: '确定', + cancelText: '', + }); + + // 抛出错误,让React错误边界捕获 + throw error; } - - // 立即开始检查状态 - checkProcessingStatus(fileIds); - - // 设置文件处理进度定时器,每10秒检查一次状态 - progressIntervalRef.current = setInterval(() => { - console.log('文件处理进度定时器触发,检查文件状态'); - checkProcessingStatus(fileIds); - }, 10000); }; // 检查文件处理状态 const checkProcessingStatus = async (fileIds: number[]) => { try { - console.log('检查文件处理状态:', fileIds); + console.log('【调试-checkProcessingStatus】检查文件处理状态:', fileIds); + + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-checkProcessingStatus】组件已卸载,取消检查'); + return; + } // 如果没有文件ID,不执行检查 if (!fileIds.length) { - console.log('没有需要检查的文件'); + console.log('【调试-checkProcessingStatus】没有需要检查的文件'); return; } // 获取文件状态 + console.log('【调试-checkProcessingStatus】发送请求获取文件状态'); const response = await getDocumentsStatus(fileIds); if (response.error) { - console.error('获取文件状态出错:', response.error); + console.error('【调试-checkProcessingStatus】获取文件状态出错:', response.error); return; } - console.log('文件状态响应:', response.data); + console.log('【调试-checkProcessingStatus】文件状态响应:', response.data); if (!response.data || !response.data.length) { - console.log('没有返回文件状态数据'); + console.log('【调试-checkProcessingStatus】没有返回文件状态数据'); return; } // 检查是否所有文件都已完成处理 const allCompleted = response.data.every(doc => doc.status === DocumentStatus.PROCESSED); + console.log('【调试-checkProcessingStatus】文件处理状态:', { allCompleted, statusList: response.data.map(doc => doc.status) }); // 更新步骤状态 if (allCompleted) { - console.log('所有文件处理完成,更新步骤状态为完成'); + console.log('【调试-checkProcessingStatus】所有文件处理完成,更新步骤状态为完成'); // 清除文件处理进度定时器 if (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); progressIntervalRef.current = null; - console.log('文件处理完成,清除文件处理进度定时器'); + console.log('【调试-checkProcessingStatus】文件处理完成,清除文件处理进度定时器'); } // 更新为全部完成状态 @@ -794,14 +1111,17 @@ export default function FilesUpload() { setUploadStage("completed"); } else { // 根据当前状态更新步骤 - updateProcessingSteps(response.data[0].status); + const currentStatus = response.data[0].status; + console.log('【调试-checkProcessingStatus】根据当前状态更新步骤:', currentStatus); + updateProcessingSteps(currentStatus); } // 刷新队列中的文件状态 updateQueueFilesStatus(response.data); } catch (error) { - console.error('检查文件处理状态出错:', error); + console.error('【调试-checkProcessingStatus】检查文件处理状态出错:', error); + // 这里不抛出错误,让定时器继续运行 } }; @@ -952,21 +1272,56 @@ export default function FilesUpload() { // 处理查看文件 const handleViewFile = async (record: Document) => { - // 检查audit_status是否为0,如果是则更新为2 - if (record.audit_status === 0 || record.audit_status === null) { - try { - const response = await updateDocumentAuditStatus(record.id.toString(), 2); - if (response.error) { - console.error('更新文件审核状态失败:', response.error); - toastService.error('更新文件审核状态失败:' + (response.error || '未知错误')); + try { + console.log('【调试-handleViewFile】开始处理查看文件,文件ID:', record.id); + + // 检查audit_status是否为0,如果是则更新为2 + if (record.audit_status === 0 || record.audit_status === null) { + try { + console.log('【调试-handleViewFile】更新文件审核状态,文件ID:', record.id); + const response = await updateDocumentAuditStatus(record.id.toString(), 2); + if (response.error) { + console.error('【调试-handleViewFile】更新文件审核状态失败:', response.error); + toastService.error('更新文件审核状态失败:' + (response.error || '未知错误')); + } else { + console.log('【调试-handleViewFile】更新文件审核状态成功'); + } + + } catch (error) { + console.error('【调试-handleViewFile】更新文件审核状态时出错:', error); + toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误')); + // 即使更新失败,也继续导航 } - - } catch (error) { - console.error('更新文件审核状态时出错:', error); - toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误')); } + + console.log(`【调试-handleViewFile】准备导航到文件详情页,文件ID: ${record.id}`); + + // 检查组件是否已卸载 + if (!isMountedRef.current) { + console.error('【调试-handleViewFile】组件已卸载,取消导航'); + return; + } + + // 使用 setTimeout 延迟导航,确保状态已更新 + setTimeout(() => { + try { + if (isMountedRef.current) { + console.log(`【调试-handleViewFile】执行导航,URL: /reviews?id=${record.id}&previousRoute=filesUpload`); + navigate(`/reviews?id=${record.id}&previousRoute=filesUpload`); + } else { + console.error('【调试-handleViewFile】组件已卸载,取消延迟导航'); + } + } catch (navError) { + console.error('【调试-handleViewFile】导航执行错误:', navError); + // 不抛出异常,防止组件崩溃 + } + }, 0); + + } catch (outerError) { + console.error('【调试-handleViewFile】查看文件处理过程中发生错误:', outerError); + toastService.error('查看文件失败:' + (outerError instanceof Error ? outerError.message : '未知错误')); + // 不抛出异常,防止组件崩溃 } - navigate(`/reviews?id=${record.id}&previousRoute=filesUpload`); }; // 表格列定义 @@ -1075,6 +1430,46 @@ export default function FilesUpload() { } ]; + // 添加路由阻止器 + const shouldBlock = uploadStage === "uploading" || uploadStage === "processing"; + + // 使用useBlocker来阻止页面导航 + const blocker = useBlocker( + ({ nextLocation }) => { + return shouldBlock && window.location.pathname !== nextLocation.pathname; + } + ); + + // 处理阻止导航的逻辑 + useEffect(() => { + if (blocker.state === "blocked") { + const confirmed = window.confirm( + "文件正在上传或处理中,离开页面将中断操作。确定要离开吗?" + ); + if (confirmed) { + blocker.proceed(); + } else { + blocker.reset(); + } + } + }, [blocker]); + + // 添加页面刷新/关闭提示 + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (shouldBlock) { + e.preventDefault(); + e.returnValue = "文件正在上传或处理中,离开页面将中断操作。确定要离开吗?"; + return e.returnValue; + } + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, [shouldBlock]); + return (
{/* 页面头部 */} @@ -1164,11 +1559,11 @@ export default function FilesUpload() { {/* 自定义标题栏 */}

文件上传

- {contractMainFiles.length > 0 && contractAttachmentFiles.length > 0 && ( + {isContractType && uploadStage === "idle" && ( @@ -1449,11 +1844,40 @@ export default function FilesUpload() { ); } -export function ErrorBoundary() { +export function ErrorBoundary({ error }: { error?: Error }) { + // 记录错误到控制台,以便开发时查看 + console.error('文件上传组件错误:', error || '未知错误'); + return (

出错了

文件上传页面加载失败。请刷新页面或联系系统管理员。

+ + {/* 在开发环境中显示错误详情 */} +
+

错误详情:

+ {error ? ( + <> +

{error.message}

+ {error.stack && ( +
{error.stack}
+ )} + + ) : ( +

未能捕获到具体错误信息,可能是 React 内部错误或异步操作中的未捕获异常

+ )} + +
+

可能的问题原因:

+
    +
  • 合同文件上传过程中的网络请求失败
  • +
  • API 响应格式与预期不符
  • +
  • 文件大小或格式问题
  • +
  • 服务器端返回了未处理的错误
  • +
+
+
+
); diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index ffbbe16..a114582 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -533,7 +533,8 @@ export default function ReviewDetails() { {reviewData.fileInfo.fileName}
- 合同编号:{reviewData.fileInfo.contractNumber} + {/* 合同编号:{reviewData.fileInfo.contractNumber} */} + 卷宗编号:{reviewData.fileInfo.contractNumber} {reviewData.fileInfo.fileSize && ( | {reviewData.fileInfo.fileSize} | {reviewData.fileInfo.fileFormat} | {reviewData.fileInfo.pageCount}页  diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..d591871 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file