410 lines
14 KiB
TypeScript
410 lines
14 KiB
TypeScript
/**
|
||
* 评查选项卡组件
|
||
* 提供三个选项卡:评查结果、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;
|
||
};
|
||
onConfirmResults: () => void;
|
||
jwtToken?: string | null;
|
||
}
|
||
|
||
export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfirmResults, jwtToken }: ReviewTabsProps) {
|
||
const [isNavigating, setIsNavigating] = useState(false);
|
||
const [isReuploadModalOpen, setIsReuploadModalOpen] = useState(false);
|
||
const [selectedTemplateFiles, setSelectedTemplateFiles] = useState<File[]>([]);
|
||
const [isUploading, setIsUploading] = 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 = previousRoute === 'documents'
|
||
? "/documents/list"
|
||
: previousRoute === 'filesUpload'
|
||
? "/files/upload"
|
||
: "/rules-files";
|
||
// 立即导航返回
|
||
navigate(returnTo);
|
||
// 触发上级页面数据重新加载
|
||
revalidator.revalidate();
|
||
};
|
||
|
||
// 下载原文件
|
||
const handleDownloadFile = async () => {
|
||
try {
|
||
// 使用 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);
|
||
alert(`下载文件失败: ${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">
|
||
<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> */}
|
||
{fileInfo.type === '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 flex items-center my-2"
|
||
onClick={handleDownloadFile}
|
||
>
|
||
<i className="ri-file-download-line mr-1"></i> 下载原文件
|
||
</button>
|
||
{/* <button
|
||
className="ant-btn ant-btn-default flex items-center"
|
||
onClick={handleExportReport}
|
||
>
|
||
<i className="ri-file-copy-line mr-1"></i> 导出评查报告
|
||
</button> */}
|
||
<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,.doc,.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、DOC、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>
|
||
);
|
||
} |