feat: 1. 实现一键替换。
2. 优化追加附件和模板上传的样式。
This commit is contained in:
@@ -337,6 +337,11 @@ export default function CrossCheckingResult() {
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
const [charPositions, setCharPositions] = useState<CharPosition[] | undefined>(undefined);
|
||||
const [localScoringProposals, setLocalScoringProposals] = useState<ScoringProposal[]>(scoring_proposals || []); // 本地状态管理scoringProposals
|
||||
const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{
|
||||
searchText: string;
|
||||
replaceText: string;
|
||||
pageNumber: number;
|
||||
} | undefined>(undefined);
|
||||
|
||||
// 使用ref来跟踪loading状态,避免不必要的重新渲染
|
||||
const isProcessingRef = useRef(false);
|
||||
@@ -432,6 +437,19 @@ export default function CrossCheckingResult() {
|
||||
setCharPositions(charPositions);
|
||||
}
|
||||
}, [activeReviewPointResultId]);
|
||||
|
||||
const handleAiSuggestionReplace = useCallback((searchText: string, replaceText: string, pageNumber: number) => {
|
||||
console.log('[CrossCheckingResult] AI建议替换:', { searchText, replaceText, pageNumber });
|
||||
setAiSuggestionReplace({
|
||||
searchText,
|
||||
replaceText,
|
||||
pageNumber
|
||||
});
|
||||
// 重置状态,避免重复触发
|
||||
setTimeout(() => {
|
||||
setAiSuggestionReplace(undefined);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
|
||||
// 处理评审点状态变更
|
||||
@@ -760,6 +778,7 @@ export default function CrossCheckingResult() {
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
targetPage={targetPage}
|
||||
charPositions={charPositions}
|
||||
aiSuggestionReplace={aiSuggestionReplace}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -775,6 +794,8 @@ export default function CrossCheckingResult() {
|
||||
jwtToken={jwtToken}
|
||||
userInfo={userInfo}
|
||||
onOpinionSubmitted={handleOpinionSubmitted}
|
||||
fileFormat={reviewData.fileInfo.fileFormat}
|
||||
onAiSuggestionReplace={handleAiSuggestionReplace}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -791,21 +791,34 @@ export default function DocumentsIndex() {
|
||||
const handleAttachmentFilesSelected = (files: FileList) => {
|
||||
try {
|
||||
console.log('【附件追加】开始处理附件文件选择, 文件数量:', files.length);
|
||||
|
||||
|
||||
if (files.length > 0) {
|
||||
// 检查主文件类型
|
||||
const selectedDocument = documents.find(doc => doc.id === selectedDocumentId);
|
||||
const isMainFileDocx = selectedDocument?.path.toLowerCase().endsWith('.docx');
|
||||
|
||||
// 验证文件类型,支持PDF、Word、ZIP、RAR
|
||||
const validFiles: File[] = [];
|
||||
let hasInvalidFiles = false;
|
||||
|
||||
let hasPdfForDocx = false;
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isValidType =
|
||||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||||
const isPdf = file.type === 'application/pdf' || fileName.endsWith('.pdf');
|
||||
const isValidType =
|
||||
isPdf ||
|
||||
// file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
||||
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
||||
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
|
||||
|
||||
|
||||
// 如果主文件是docx,不允许上传pdf附件
|
||||
if (isMainFileDocx && isPdf) {
|
||||
hasPdfForDocx = true;
|
||||
console.error(`【附件追加】主文件为DOCX格式时不允许上传PDF附件: ${file.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidType) {
|
||||
validFiles.push(file);
|
||||
} else {
|
||||
@@ -813,15 +826,21 @@ export default function DocumentsIndex() {
|
||||
console.error(`【附件追加】无效的文件类型: ${file.name}, 类型: ${file.type}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasInvalidFiles) {
|
||||
|
||||
if (hasPdfForDocx) {
|
||||
messageService.error('主文件为DOCX格式时,附件不可以是PDF格式', {
|
||||
title: '文件类型限制',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
});
|
||||
} else if (hasInvalidFiles) {
|
||||
messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
setAttachmentFiles(validFiles);
|
||||
console.log('【附件追加】有效文件数量:', validFiles.length);
|
||||
@@ -887,14 +906,14 @@ export default function DocumentsIndex() {
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isValidType =
|
||||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||||
// file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
|
||||
|
||||
if (isValidType) {
|
||||
setTemplateFile(file);
|
||||
console.log('【合同模板上传】有效文件:', file.name);
|
||||
} else {
|
||||
messageService.error('只支持PDF、Word格式的文件', {
|
||||
messageService.error('只支持.pdf、.docx格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
@@ -1584,7 +1603,7 @@ export default function DocumentsIndex() {
|
||||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
支持PDF、Word、ZIP、RAR格式,ZIP/RAR内仅合并其中的PDF文件
|
||||
支持.pdf、.docx、ZIP、RAR格式。ZIP/RAR内需要保证文件格式一致,否则报错
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1732,7 +1751,7 @@ export default function DocumentsIndex() {
|
||||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
支持PDF、Word格式,用于与合同文档进行结构对比
|
||||
支持.pdf、.docx格式,用于与合同文档进行结构对比
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1744,7 +1763,7 @@ export default function DocumentsIndex() {
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors">
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.doc,.docx"
|
||||
accept=".pdf,.docx"
|
||||
onChange={(e) => e.target.files && handleTemplateFileSelected(e.target.files)}
|
||||
className="hidden"
|
||||
id="template-file-input"
|
||||
@@ -1752,7 +1771,7 @@ export default function DocumentsIndex() {
|
||||
<label htmlFor="template-file-input" className="cursor-pointer">
|
||||
<i className="ri-file-copy-line text-3xl text-gray-400 mb-2 block"></i>
|
||||
<p className="text-sm text-gray-600">点击选择文件或拖拽文件到此处</p>
|
||||
<p className="text-xs text-gray-500 mt-1">支持PDF、Word格式</p>
|
||||
<p className="text-xs text-gray-500 mt-1">支持.pdf、.docx格式</p>
|
||||
</label>
|
||||
</div>
|
||||
{templateFile && (
|
||||
|
||||
+128
-34
@@ -35,7 +35,7 @@ export function links() {
|
||||
|
||||
// 面包屑导航
|
||||
export const handle = {
|
||||
breadcrumb: "文档列表",
|
||||
breadcrumb: "文档上传",
|
||||
previousRoute: {
|
||||
title: "文档列表",
|
||||
to: "/documents/list"
|
||||
@@ -647,7 +647,7 @@ export default function FilesUpload() {
|
||||
|
||||
if (hasInvalidFiles) {
|
||||
// 显示错误提示
|
||||
messageService.error('只支持PDF、Word格式的文件', {
|
||||
messageService.error('只支持.pdf、.docx格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
@@ -742,7 +742,7 @@ export default function FilesUpload() {
|
||||
if (hasInvalidFiles) {
|
||||
// 显示错误提示
|
||||
console.error('【调试-handleContractMainFilesSelected】存在无效的文件类型');
|
||||
messageService.error('只支持PDF、Word格式的文件', {
|
||||
messageService.error('只支持.pdf、.docx格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
@@ -799,7 +799,7 @@ export default function FilesUpload() {
|
||||
if (hasInvalidFiles) {
|
||||
// 显示错误提示
|
||||
console.error('【调试-handleContractAttachmentFilesSelected】存在无效的文件类型');
|
||||
messageService.error('只支持PDF、Word格式的文件', {
|
||||
messageService.error('只支持.pdf、.docx格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
@@ -856,7 +856,7 @@ export default function FilesUpload() {
|
||||
if (hasInvalidFiles) {
|
||||
// 显示错误提示
|
||||
console.error('【调试-handleContractTemplateFilesSelected】存在无效的文件类型');
|
||||
messageService.error('只支持PDF、Word格式的文件', {
|
||||
messageService.error('只支持.pdf、.docx格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
@@ -883,21 +883,34 @@ export default function FilesUpload() {
|
||||
const handleAttachmentFilesSelected = (files: FileList) => {
|
||||
try {
|
||||
console.log('【附件追加】开始处理附件文件选择, 文件数量:', files.length);
|
||||
|
||||
|
||||
if (files.length > 0) {
|
||||
// 检查主文件类型
|
||||
const selectedDocument = queueFiles.find(doc => doc.id === selectedDocumentId);
|
||||
const isMainFileDocx = selectedDocument?.path.toLowerCase().endsWith('.docx');
|
||||
|
||||
// 验证文件类型,支持PDF、Word、ZIP、RAR
|
||||
const validFiles: File[] = [];
|
||||
let hasInvalidFiles = false;
|
||||
|
||||
let hasPdfForDocx = false;
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isValidType =
|
||||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||||
const isPdf = file.type === 'application/pdf' || fileName.endsWith('.pdf');
|
||||
const isValidType =
|
||||
isPdf ||
|
||||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
||||
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
||||
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
|
||||
|
||||
|
||||
// 如果主文件是docx,不允许上传pdf附件
|
||||
if (isMainFileDocx && isPdf) {
|
||||
hasPdfForDocx = true;
|
||||
console.error(`【附件追加】主文件为DOCX格式时不允许上传PDF附件: ${file.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidType) {
|
||||
validFiles.push(file);
|
||||
} else {
|
||||
@@ -905,15 +918,21 @@ export default function FilesUpload() {
|
||||
console.error(`【附件追加】无效的文件类型: ${file.name}, 类型: ${file.type}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (hasInvalidFiles) {
|
||||
|
||||
if (hasPdfForDocx) {
|
||||
messageService.error('主文件为DOCX格式时,附件不可以是PDF格式', {
|
||||
title: '文件类型限制',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
});
|
||||
} else if (hasInvalidFiles) {
|
||||
messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
setAttachmentFiles(validFiles);
|
||||
console.log('【附件追加】有效文件数量:', validFiles.length);
|
||||
@@ -984,7 +1003,7 @@ export default function FilesUpload() {
|
||||
setTemplateFile(file);
|
||||
console.log('【合同模板上传】有效文件:', file.name);
|
||||
} else {
|
||||
messageService.error('只支持PDF、Word格式的文件', {
|
||||
messageService.error('只支持.pdf、.docx格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
@@ -1166,6 +1185,11 @@ export default function FilesUpload() {
|
||||
clearInterval(uploadProgressIntervalRef.current);
|
||||
uploadProgressIntervalRef.current = null;
|
||||
}
|
||||
|
||||
// 清空合同模板文件缓存
|
||||
setContractTemplateFiles([]);
|
||||
console.log('【合同上传失败】已清空合同模板文件缓存');
|
||||
|
||||
resetUpload();
|
||||
}
|
||||
};
|
||||
@@ -1304,7 +1328,7 @@ export default function FilesUpload() {
|
||||
|
||||
if (invalidFiles.length > 0) {
|
||||
console.error('【调试-startUpload】文件类型验证失败:', invalidFiles.map(f => f.name));
|
||||
throw new Error('只支持PDF、Word格式的文件');
|
||||
throw new Error('只支持.pdf、.docx格式的文件');
|
||||
}
|
||||
|
||||
setUploadStage("uploading");
|
||||
@@ -2235,7 +2259,7 @@ export default function FilesUpload() {
|
||||
ref={uploadAreaRef}
|
||||
onFilesSelected={handleFilesSelected}
|
||||
multiple={true}
|
||||
accept=".pdf,.doc,.docx"
|
||||
accept=".pdf,.docx"
|
||||
tipText="支持单个或多个文件上传,文件格式:PDF/Word"
|
||||
shouldPreventFileSelect={!fileType}
|
||||
/>
|
||||
@@ -2243,12 +2267,29 @@ export default function FilesUpload() {
|
||||
// 合同文件上传区域 - 三区域布局
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">合同主文件</h4>
|
||||
<UploadArea
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<h4 className="font-medium">合同主文件</h4>
|
||||
{contractMainFiles.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setContractMainFiles([]);
|
||||
if (contractMainFileRef.current) {
|
||||
contractMainFileRef.current.resetFileInput();
|
||||
}
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 transition-colors"
|
||||
title="清空已选择的主文件"
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<UploadArea
|
||||
onFilesSelected={handleContractMainFilesSelected}
|
||||
ref={contractMainFileRef}
|
||||
multiple={false}
|
||||
accept=".pdf,.doc,.docx"
|
||||
accept=".pdf,.docx"
|
||||
tipText="请上传合同主文件,格式:PDF/Word"
|
||||
mainText="上传合同主文件"
|
||||
buttonText="选择主文件"
|
||||
@@ -2257,18 +2298,35 @@ export default function FilesUpload() {
|
||||
/>
|
||||
{contractMainFiles.length > 0 && (
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
已选择主文件: <span className="font-medium">{contractMainFiles[0].name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">合同附件</h4>
|
||||
<UploadArea
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<h4 className="font-medium">合同附件</h4>
|
||||
{contractAttachmentFiles.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setContractAttachmentFiles([]);
|
||||
if (contractAttachmentFileRef.current) {
|
||||
contractAttachmentFileRef.current.resetFileInput();
|
||||
}
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 transition-colors"
|
||||
title="清空已选择的附件"
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<UploadArea
|
||||
onFilesSelected={handleContractAttachmentFilesSelected}
|
||||
ref={contractAttachmentFileRef}
|
||||
multiple={false}
|
||||
accept=".pdf,.doc,.docx"
|
||||
accept=".pdf,.docx"
|
||||
tipText="请上传合同附件,格式:PDF/Word"
|
||||
mainText="上传合同附件"
|
||||
buttonText="选择附件"
|
||||
@@ -2277,7 +2335,7 @@ export default function FilesUpload() {
|
||||
/>
|
||||
{contractAttachmentFiles.length > 0 && (
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
已选择附件: {contractAttachmentFiles.map((file, index) => (
|
||||
<span key={index} className="font-medium">{file.name}</span>
|
||||
))}
|
||||
@@ -2285,11 +2343,25 @@ export default function FilesUpload() {
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">合同模板</h4>
|
||||
<UploadArea
|
||||
<div className="flex gap-4 items-center mb-2">
|
||||
<h4 className="font-medium">合同模板</h4>
|
||||
{contractTemplateFiles.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setContractTemplateFiles([]);
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 transition-colors"
|
||||
title="清空已选择的模板"
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<UploadArea
|
||||
onFilesSelected={handleContractTemplateFilesSelected}
|
||||
multiple={false}
|
||||
accept=".pdf,.doc,.docx"
|
||||
accept=".pdf,.docx"
|
||||
tipText="请上传合同模板,格式:PDF/Word"
|
||||
mainText="上传合同模板"
|
||||
buttonText="选择模板"
|
||||
@@ -2298,7 +2370,7 @@ export default function FilesUpload() {
|
||||
/>
|
||||
{contractTemplateFiles.length > 0 && (
|
||||
<div className="mt-2 text-sm text-green-600">
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
已选择模板: <span className="font-medium">{contractTemplateFiles[0].name}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -2521,7 +2593,19 @@ export default function FilesUpload() {
|
||||
|
||||
{/* 附件追加模态框 */}
|
||||
{showAttachmentUpload && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
|
||||
onClick={(e) => {
|
||||
// 点击蒙层关闭模态框
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowAttachmentUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
setAttachmentFiles([]);
|
||||
setAttachmentRemark("");
|
||||
setAttachmentMergeMode('overwrite');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">追加合同附件</h3>
|
||||
@@ -2557,7 +2641,7 @@ export default function FilesUpload() {
|
||||
<UploadArea
|
||||
onFilesSelected={handleAttachmentFilesSelected}
|
||||
multiple={true}
|
||||
accept=".pdf,.doc,.docx,.zip,.rar"
|
||||
accept=".pdf,.docx,.zip,.rar"
|
||||
tipText="支持PDF、Word、ZIP、RAR格式,可多选"
|
||||
mainText="选择附件文件"
|
||||
buttonText="选择文件"
|
||||
@@ -2655,7 +2739,17 @@ export default function FilesUpload() {
|
||||
|
||||
{/* 合同模板上传模态框 */}
|
||||
{showTemplateUpload && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
|
||||
onClick={(e) => {
|
||||
// 点击蒙层关闭模态框
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowTemplateUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
setTemplateFile(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-lg mx-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">上传合同模板</h3>
|
||||
@@ -2678,7 +2772,7 @@ export default function FilesUpload() {
|
||||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
支持PDF、Word格式,用于与合同文档进行结构对比
|
||||
支持.pdf、.docx格式,用于与合同文档进行结构对比
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2690,8 +2784,8 @@ export default function FilesUpload() {
|
||||
<UploadArea
|
||||
onFilesSelected={handleTemplateFileSelected}
|
||||
multiple={false}
|
||||
accept=".pdf,.doc,.docx"
|
||||
tipText="支持PDF、Word格式"
|
||||
accept=".pdf,.docx"
|
||||
tipText="支持.pdf、.docx格式"
|
||||
mainText="选择模板文件"
|
||||
buttonText="选择文件"
|
||||
icon="ri-file-copy-line"
|
||||
|
||||
@@ -8,14 +8,25 @@
|
||||
* - 后续可扩展文件上传功能
|
||||
*/
|
||||
|
||||
import { type MetaFunction } from "@remix-run/node";
|
||||
import { type MetaFunction, type ClientLoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { DiffEditor } from "@monaco-editor/react";
|
||||
import type { editor } from "monaco-editor";
|
||||
import { pdfjs } from 'react-pdf';
|
||||
import mammoth from 'mammoth';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
|
||||
// 动态导入 Monaco Editor(仅客户端)
|
||||
let DiffEditor: any = null;
|
||||
let editor: any = null;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
import('@monaco-editor/react').then((mod) => {
|
||||
DiffEditor = mod.DiffEditor;
|
||||
});
|
||||
import('monaco-editor').then((mod) => {
|
||||
editor = mod.editor;
|
||||
});
|
||||
}
|
||||
|
||||
// 设置 PDF.js worker(与 pdf-demo.tsx 相同)
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||
|
||||
@@ -129,9 +140,10 @@ const CONTRACT_B = `中国烟草合同(修订版本)
|
||||
export default function MonacoDemoPage() {
|
||||
const [originalText, setOriginalText] = useState(CONTRACT_A);
|
||||
const [modifiedText, setModifiedText] = useState(CONTRACT_B);
|
||||
const diffEditorRef = useRef<editor.IStandaloneDiffEditor | null>(null);
|
||||
const diffEditorRef = useRef<any>(null);
|
||||
const [diffCount, setDiffCount] = useState<number>(0);
|
||||
const [currentDiff, setCurrentDiff] = useState<number>(0);
|
||||
const [editorLoaded, setEditorLoaded] = useState(false);
|
||||
|
||||
// 文档相关状态
|
||||
// 默认使用的测试文档路径(相对路径)
|
||||
@@ -297,9 +309,23 @@ export default function MonacoDemoPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 检查 Monaco Editor 是否已加载
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
Promise.all([
|
||||
import('@monaco-editor/react'),
|
||||
import('monaco-editor')
|
||||
]).then(([reactMod, editorMod]) => {
|
||||
DiffEditor = reactMod.DiffEditor;
|
||||
editor = editorMod.editor;
|
||||
setEditorLoaded(true);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Monaco Editor 挂载后的回调
|
||||
const handleEditorDidMount = (editor: editor.IStandaloneDiffEditor) => {
|
||||
diffEditorRef.current = editor;
|
||||
const handleEditorDidMount = (editorInstance: any) => {
|
||||
diffEditorRef.current = editorInstance;
|
||||
|
||||
// 获取差异数量
|
||||
const lineChanges = editor.getLineChanges();
|
||||
@@ -670,7 +696,8 @@ export default function MonacoDemoPage() {
|
||||
|
||||
{/* Diff Editor 主体 */}
|
||||
<div style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
|
||||
<DiffEditor
|
||||
{editorLoaded && DiffEditor ? (
|
||||
<DiffEditor
|
||||
height="100%"
|
||||
language="plaintext"
|
||||
original={originalText}
|
||||
@@ -699,6 +726,30 @@ export default function MonacoDemoPage() {
|
||||
diffAlgorithm: 'advanced', // 使用高级差异算法
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
backgroundColor: '#f5f5f5'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
border: '4px solid #f3f3f3',
|
||||
borderTop: '4px solid #00684a',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
margin: '0 auto 16px'
|
||||
}}></div>
|
||||
<div style={{ fontSize: '16px', color: '#333' }}>
|
||||
正在加载 Monaco Editor...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文档加载中的遮罩层 */}
|
||||
{(isLoadingDoc1 || isLoadingDoc2) && (
|
||||
|
||||
+25
-1
@@ -315,6 +315,11 @@ export default function ReviewDetails() {
|
||||
newStatus: string;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{
|
||||
searchText: string;
|
||||
replaceText: string;
|
||||
pageNumber: number;
|
||||
} | undefined>(undefined);
|
||||
|
||||
// 🐛 调试:打印 loader 返回的完整数据到浏览器控制台
|
||||
useEffect(() => {
|
||||
@@ -432,7 +437,22 @@ export default function ReviewDetails() {
|
||||
setHighlightValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 处理AI建议替换
|
||||
const handleAiSuggestionReplace = (searchText: string, replaceText: string, pageNumber: number) => {
|
||||
console.log('[Reviews] AI建议替换:', { searchText, replaceText, pageNumber });
|
||||
// 设置替换参数,触发 CollaboraViewer 的搜索替换
|
||||
setAiSuggestionReplace({
|
||||
searchText,
|
||||
replaceText,
|
||||
pageNumber
|
||||
});
|
||||
// 短暂延迟后清除参数,以便下次可以重新触发
|
||||
setTimeout(() => {
|
||||
setAiSuggestionReplace(undefined);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 刷新评审数据
|
||||
// async function refreshReviewData(documentId: string) {
|
||||
// // 设置加载状态
|
||||
@@ -781,6 +801,7 @@ export default function ReviewDetails() {
|
||||
charPositions={charPositions}
|
||||
highlightValue={highlightValue}
|
||||
userInfo={loaderData.userInfo}
|
||||
aiSuggestionReplace={aiSuggestionReplace}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
@@ -788,12 +809,15 @@ export default function ReviewDetails() {
|
||||
|
||||
{/* 右侧:评查结果 */}
|
||||
<div className="w-full lg:w-[35%]">
|
||||
{/* {JSON.stringify(reviewData.fileInfo.fileFormat)} */}
|
||||
<ReviewPointsList
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
statistics={reviewData.statistics}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
onReviewPointSelect={handleReviewPointSelect}
|
||||
onStatusChange={handleReviewPointStatusChange}
|
||||
fileFormat={reviewData.fileInfo.fileFormat}
|
||||
onAiSuggestionReplace={handleAiSuggestionReplace}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user