Files
2026-05-07 18:18:35 +08:00

469 lines
16 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 评查选项卡组件
* 提供三个选项卡:评查结果、AI智能分析、文件信息
*/
import { ReactNode, useState, useRef } from 'react';
// import { useNavigate, useRevalidator } from 'react-router-dom';
import { useNavigate, useRevalidator } from '@remix-run/react';
import { loadingBarService } from '~/components/ui/LoadingBar';
import { Modal } from '~/components/ui/Modal';
import { UploadArea, type UploadAreaRef } from '~/components/ui/UploadArea';
import { Button } from '~/components/ui/Button';
import { toastService } from '~/components/ui/Toast';
// import { DOCUMENT_URL } from "~/api/axios-client";
import { uploadContractTemplate } from '~/api/files/files-upload';
import axios from 'axios';
interface ReviewTabsProps {
activeTab: string;
onTabChange: (tabKey: string) => void;
children: ReactNode;
fileInfo: {
id?: number;
previousRoute?: string;
path?: string;
auditStatus?: number;
type?: string;
comparisonId?: number;
backTo?: string;
};
onConfirmResults: () => void;
onExportReport?: () => void;
jwtToken?: string | null;
showConfirmButton?: boolean;
showCompareTab?: boolean;
/** 下载前保存文档的回调,返回 true 表示保存成功可以继续下载 */
onSaveBeforeDownload?: () => Promise<boolean>;
}
export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfirmResults, onExportReport, jwtToken, showConfirmButton = true, showCompareTab = true, onSaveBeforeDownload }: ReviewTabsProps) {
const [isNavigating, setIsNavigating] = useState(false);
const [isReuploadModalOpen, setIsReuploadModalOpen] = useState(false);
const [selectedTemplateFiles, setSelectedTemplateFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false);
const [isSaving, setIsSaving] = useState(false); // 下载前保存文档的状态
const uploadAreaRef = useRef<UploadAreaRef>(null);
const navigate = useNavigate();
const revalidator = useRevalidator();
// 返回上一级
const handleBack = () => {
// 防抖处理 - 如果已经在导航中,不重复触发
if (isNavigating) return;
// 设置导航状态为true
setIsNavigating(true);
loadingBarService.show();
// 根据来源页面返回
const previousRoute = fileInfo.previousRoute || 'documents';
const returnTo = fileInfo.backTo || (
previousRoute === 'documents'
? "/documents/list"
: previousRoute === 'filesUpload'
? "/files/upload"
: previousRoute === 'crossChecking'
? "/cross-checking"
: "/rules-files"
);
navigate(returnTo);
setTimeout(() => {
revalidator.revalidate();
setIsNavigating(false);
loadingBarService.hide();
}, 0);
};
// 下载原文件
const handleDownloadFile = async () => {
if (!fileInfo.path) {
toastService.warning('当前文档暂无可下载原文件');
return;
}
try {
// 如果有保存回调,先执行保存(仅对 DOCX 文件有效)
if (onSaveBeforeDownload) {
setIsSaving(true);
toastService.info('正在保存文档...');
const saveSuccess = await onSaveBeforeDownload();
setIsSaving(false);
if (!saveSuccess) {
toastService.error('保存失败,下载已取消');
return;
}
toastService.success('文档已保存,开始下载...');
}
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(fileInfo.path || '')}`;
// 使用axios获取文件内容
const response = await axios.get(downloadUrl, {
responseType: 'blob'
});
// axios已经返回Blob
const blob = response.data;
// 创建Blob URL
const blobUrl = URL.createObjectURL(blob);
// 创建一个隐藏的a标签并点击它
const a = document.createElement('a');
a.style.display = 'none';
a.href = blobUrl;
// 从路径中获取文件名
const fileName = fileInfo.path?.split('/').pop() || 'document';
a.download = decodeURIComponent(fileName);
document.body.appendChild(a);
a.click();
// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
}, 100);
} catch (error) {
console.error('下载文件失败:', error);
toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
};
// 打开重新上传模板模态框
const handleOpenReuploadModal = () => {
setIsReuploadModalOpen(true);
setSelectedTemplateFiles([]);
};
// 关闭重新上传模板模态框
const handleCloseReuploadModal = () => {
setIsReuploadModalOpen(false);
setSelectedTemplateFiles([]);
// 重置文件输入
if (uploadAreaRef.current) {
uploadAreaRef.current.resetFileInput();
}
};
// 处理模板文件选择
const handleTemplateFilesSelected = (files: FileList) => {
try {
if (files.length > 0) {
// 验证文件类型,允许PDF和Word文件
const validFiles: File[] = [];
let hasInvalidFiles = false;
Array.from(files).forEach(file => {
const fileName = file.name.toLowerCase();
const isValidType =
file.type === 'application/pdf' ||
fileName.endsWith('.pdf') ||
file.type === 'application/msword' ||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
fileName.endsWith('.doc') ||
fileName.endsWith('.docx');
if (isValidType) {
validFiles.push(file);
} else {
hasInvalidFiles = true;
console.error(`无效的文件类型: ${file.name}, 类型: ${file.type}`);
}
});
if (hasInvalidFiles) {
toastService.error('只能上传PDF或Word格式的文件');
}
if (validFiles.length > 0) {
setSelectedTemplateFiles(validFiles);
}
}
} catch (error) {
console.error('处理模板文件选择时发生错误:', error);
toastService.error('文件选择失败,请重试');
}
};
// 确认上传模板文件
const handleConfirmUpload = async () => {
if (selectedTemplateFiles.length === 0) {
toastService.error('请先选择要上传的模板文件');
return;
}
// 验证必需的参数
if (!fileInfo.id) {
toastService.error('文档ID不能为空');
return;
}
try {
setIsUploading(true);
console.log('【重新上传模板】开始上传:', {
fileName: selectedTemplateFiles[0].name,
documentId: fileInfo.id,
comparisonId: fileInfo.comparisonId
});
// 调用专门的合同模板上传接口
const uploadResult = await uploadContractTemplate(
selectedTemplateFiles[0], // file: File 对象
fileInfo.id, // documentId: 合同文件的id
fileInfo.comparisonId, // comparisonId: 模板预览文件的id (可选)
jwtToken || undefined // jwtToken
);
console.log('【重新上传模板】上传结果:', uploadResult);
if (uploadResult.error) {
throw new Error(uploadResult.error);
}
toastService.success('模板文件上传成功,结构比对数据将会发生更新,即将返回上一页...');
await new Promise(resolve => setTimeout(resolve, 2000));
handleCloseReuploadModal();
handleBack();
} catch (error) {
console.error('上传模板文件失败:', error);
toastService.error(`上传失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setIsUploading(false);
}
};
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
return (
<div className="tab-container w-full flex-1">
<div className="tab-nav w-full flex justify-between">
{/* 评查结果、AI智能分析、文件信息 */}
<div className="flex">
{/* {JSON.stringify(fileInfo)} */}
<button
className={`tab-nav-item ${activeTab === 'preview' ? 'active' : ''}`}
onClick={() => onTabChange('preview')}
type="button"
aria-pressed={activeTab === 'preview'}
>
<i className="ri-file-text-line"></i>
</button>
{/* <button
className={`tab-nav-item ${activeTab === 'analysis' ? 'active' : ''}`}
onClick={() => onTabChange('analysis')}
type="button"
aria-pressed={activeTab === 'analysis'}
>
<i className="ri-lightbulb-line"></i> AI智能分析
</button> */}
{showCompareTab && fileInfo.type?.toString().includes('1') && (
<button
className={`tab-nav-item ${activeTab === 'filecompare' ? 'active' : ''}`}
onClick={() => onTabChange('filecompare')}
type="button"
aria-pressed={activeTab === 'filecompare'}
>
<i className="ri-flip-horizontal-line"></i>
</button>
)}
<button
className={`tab-nav-item ${activeTab === 'fileinfo' ? 'active' : ''}`}
onClick={() => onTabChange('fileinfo')}
type="button"
aria-pressed={activeTab === 'fileinfo'}
>
<i className="ri-information-line"></i>
</button>
</div>
{/* 操作按钮 */}
<div className="flex space-x-3">
{/* 重新上传 */}
{activeTab === 'filecompare' && (
<button
className="ant-btn ant-btn-default flex items-center my-2 mr-4"
onClick={handleOpenReuploadModal}
>
<i className="ri-refresh-line mr-1"></i>
</button>
)}
{/* 返回上一级 */}
<button
className="ant-btn ant-btn-default flex items-center my-2"
onClick={() => handleBack()}
disabled={isNavigating}
>
<i className="ri-arrow-left-line mr-1"></i> {isNavigating ? '返回中...' : '返回'}
</button>
<button
className="ant-btn ant-btn-default inline-flex items-center my-2"
onClick={handleDownloadFile}
disabled={isSaving}
>
{isSaving ? (
<>
<i className="ri-loader-4-line mr-1 animate-spin"></i> ...
</>
) : (
<>
<i className="ri-file-download-line mr-1"></i>
</>
)}
</button>
{onExportReport && (
<button
className="ant-btn ant-btn-default inline-flex items-center my-2"
onClick={onExportReport}
disabled={isNavigating}
>
<i className="ri-file-copy-line mr-1"></i>
</button>
)}
{showConfirmButton && (
<button
className={`ant-btn ant-btn-primary my-2 flex items-center ${fileInfo.auditStatus === 1 ? 'hidden' : ''}`}
onClick={onConfirmResults}
>
<i className="ri-check-double-line mr-1"></i>
</button>
)}
{showConfirmButton && (
<button
className={`ant-btn ant-btn-primary my-2 flex items-center ${fileInfo.auditStatus === 1 ? 'hidden' : ''}`}
onClick={onConfirmResults}
>
<i className="ri-check-double-line mr-1"></i>
</button>
)}
</div>
</div>
<div className="tab-content w-full">
{children}
</div>
{/* 重新上传模板模态框 */}
<Modal
isOpen={isReuploadModalOpen}
onClose={handleCloseReuploadModal}
title="重新上传模板"
size="medium"
footer={
<div className="flex justify-end space-x-3">
<Button
type="default"
onClick={handleCloseReuploadModal}
disabled={isUploading}
>
</Button>
<Button
type="primary"
onClick={handleConfirmUpload}
disabled={selectedTemplateFiles.length === 0 || isUploading}
icon={isUploading ? 'ri-loader-4-line animate-spin' : undefined}
>
{isUploading ? '上传中...' : '确定上传'}
</Button>
</div>
}
>
<div className="space-y-4">
<div className="text-sm text-gray-600 mb-4">
<p></p>
<p className="mt-2 text-orange-600">
<i className="ri-information-line mr-1"></i>
PDF和Word格式的文件
</p>
</div>
<UploadArea
ref={uploadAreaRef}
onFilesSelected={handleTemplateFilesSelected}
accept=".pdf,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
multiple={false}
icon="ri-file-text-line"
buttonText="选择模板文件"
mainText="点击或拖拽文件到此区域"
tipText={
<span className="text-xs text-gray-500">
.pdf | .docx
</span>
}
disabled={isUploading}
/>
{/* 已选择的文件列表 */}
{selectedTemplateFiles.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-medium text-gray-900 mb-2"></h4>
<div className="space-y-2">
{selectedTemplateFiles.map((file, index) => {
// 根据文件类型确定图标和颜色
const fileName = file.name.toLowerCase();
let fileIcon = 'ri-file-pdf-line';
let iconColor = 'text-red-500';
if (fileName.endsWith('.doc') || fileName.endsWith('.docx')) {
fileIcon = 'ri-file-word-2-line';
iconColor = 'text-blue-600';
}
return (
<div
key={index}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border"
>
<div className="flex items-center">
<i className={`${fileIcon} ${iconColor} mr-2`}></i>
<div>
<div className="text-sm font-medium text-gray-900">
{file.name}
</div>
<div className="text-xs text-gray-500">
{formatFileSize(file.size)}
</div>
</div>
</div>
<button
className="text-gray-400 hover:text-red-500 transition-colors"
onClick={() => {
setSelectedTemplateFiles(prev =>
prev.filter((_, i) => i !== index)
);
if (uploadAreaRef.current) {
uploadAreaRef.current.resetFileInput();
}
}}
disabled={isUploading}
>
<i className="ri-close-line"></i>
</button>
</div>
);
})}
</div>
</div>
)}
</div>
</Modal>
</div>
);
}