feat: 1. 接入CollaboraViewer选中的高亮效果,清除高亮功能,页面销毁自动清除高亮。
2. 合同模板对比接入monaco editor的效果。 3. 添加交叉评查的案卷类型的数据查询。 fix: 1. 修复文档列表的打开模态框蒙板层显示效果。
This commit is contained in:
@@ -0,0 +1,701 @@
|
||||
/**
|
||||
* ComparePreview - Document Comparison Preview Component
|
||||
*
|
||||
* Features:
|
||||
* - Compare two documents using Monaco Editor
|
||||
* - Support PDF and Word (.docx) files
|
||||
* - Automatic text extraction and line-by-line comparison
|
||||
* - Navigation between differences
|
||||
*
|
||||
* Props:
|
||||
* - doc1Path: Original document path
|
||||
* - doc2Path: Comparison document path
|
||||
*/
|
||||
|
||||
import React, { 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';
|
||||
import { DOCUMENT_URL } from '~/config/api-config';
|
||||
|
||||
// Setup PDF.js worker
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||
|
||||
// Document type enum
|
||||
type DocumentType = 'pdf' | 'docx' | 'unknown';
|
||||
|
||||
// PDF type enum
|
||||
type PdfType = 'text' | 'scanned' | 'unknown';
|
||||
|
||||
// PDF info interface
|
||||
interface PdfInfo {
|
||||
type: PdfType;
|
||||
numPages: number;
|
||||
textLength: number;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
// Document info interface
|
||||
interface DocumentInfo {
|
||||
fileType: DocumentType;
|
||||
pdfType?: PdfType;
|
||||
numPages?: number;
|
||||
textLength: number;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
// Component Props interface
|
||||
interface ComparePreviewProps {
|
||||
doc1Path: string;
|
||||
doc2Path: string;
|
||||
}
|
||||
|
||||
export function ComparePreview({ doc1Path, doc2Path }: ComparePreviewProps): JSX.Element {
|
||||
// 如果没有模板合同路径,直接返回提示
|
||||
if (!doc2Path || doc2Path.trim() === '') {
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '40px',
|
||||
backgroundColor: '#f9f9f9'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '500px',
|
||||
textAlign: 'center',
|
||||
padding: '32px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid #e0e0e0'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
margin: '0 auto 24px',
|
||||
backgroundColor: '#fff3cd',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '3px solid #ffc107'
|
||||
}}>
|
||||
<i className="ri-file-warning-line" style={{ fontSize: '32px', color: '#ff9800' }}></i>
|
||||
</div>
|
||||
<h3 style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
无法进行对比
|
||||
</h3>
|
||||
<p style={{
|
||||
fontSize: '14px',
|
||||
color: '#666',
|
||||
lineHeight: '1.6',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
该文档类型暂未上传模板合同,无法进行对比分析。
|
||||
</p>
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffc107',
|
||||
borderRadius: '4px',
|
||||
fontSize: '13px',
|
||||
color: '#856404',
|
||||
lineHeight: '1.5'
|
||||
}}>
|
||||
<i className="ri-error-warning-line" style={{ marginRight: '6px' }}></i>
|
||||
<strong>提示:</strong>请先在文档类型管理中为该类型上传模板合同,然后重新加载此页面。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [originalText, setOriginalText] = useState<string>('');
|
||||
const [modifiedText, setModifiedText] = useState<string>('');
|
||||
const diffEditorRef = useRef<editor.IStandaloneDiffEditor | null>(null);
|
||||
const [diffCount, setDiffCount] = useState<number>(0);
|
||||
const [currentDiff, setCurrentDiff] = useState<number>(0);
|
||||
|
||||
const [doc1Info, setDoc1Info] = useState<DocumentInfo | null>(null);
|
||||
const [doc2Info, setDoc2Info] = useState<DocumentInfo | null>(null);
|
||||
const [isLoadingDoc1, setIsLoadingDoc1] = useState(false);
|
||||
const [isLoadingDoc2, setIsLoadingDoc2] = useState(false);
|
||||
|
||||
// Log initial props
|
||||
// console.log('[ComparePreview] Component initialized with paths:', { doc1Path, doc2Path });
|
||||
|
||||
// Detect file type based on file path
|
||||
const detectFileType = (filePath: string): DocumentType => {
|
||||
const lowerPath = filePath.toLowerCase();
|
||||
if (lowerPath.endsWith('.pdf')) return 'pdf';
|
||||
if (lowerPath.endsWith('.docx') || lowerPath.endsWith('.doc')) return 'docx';
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
// PDF type detection function
|
||||
const detectPdfType = async (pdfUrl: string): Promise<PdfInfo> => {
|
||||
const loadingTask = pdfjs.getDocument(pdfUrl);
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
let totalTextLength = 0;
|
||||
const pagesToCheck = Math.min(pdf.numPages, 3);
|
||||
|
||||
for (let i = 1; i <= pagesToCheck; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const textContent = await page.getTextContent();
|
||||
const pageText = textContent.items
|
||||
.map((item: any) => item.str)
|
||||
.join('');
|
||||
totalTextLength += pageText.length;
|
||||
}
|
||||
|
||||
const avgTextPerPage = totalTextLength / pagesToCheck;
|
||||
const confidence = Math.min(avgTextPerPage / 500, 1);
|
||||
|
||||
let type: PdfType;
|
||||
if (avgTextPerPage > 100) {
|
||||
type = 'text';
|
||||
} else if (avgTextPerPage > 10) {
|
||||
type = 'scanned';
|
||||
} else {
|
||||
type = 'unknown';
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
numPages: pdf.numPages,
|
||||
textLength: totalTextLength,
|
||||
confidence
|
||||
};
|
||||
};
|
||||
|
||||
// PDF text extraction function
|
||||
const extractTextFromPdf = async (pdfUrl: string): Promise<string> => {
|
||||
const loadingTask = pdfjs.getDocument(pdfUrl);
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
let fullText = '';
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const textContent = await page.getTextContent();
|
||||
const pageText = textContent.items
|
||||
.map((item: any) => item.str)
|
||||
.join(' ');
|
||||
fullText += `\n========== Page ${i} ==========\n${pageText}\n`;
|
||||
}
|
||||
|
||||
return fullText;
|
||||
};
|
||||
|
||||
// Word document text extraction function
|
||||
const extractTextFromWord = async (docUrl: string): Promise<string> => {
|
||||
const response = await fetch(docUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Cannot load document: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const textResult = await mammoth.extractRawText({ arrayBuffer });
|
||||
|
||||
return textResult.value;
|
||||
};
|
||||
|
||||
// Load document and extract text (supports PDF and Word)
|
||||
const loadDocumentAndExtractText = async (
|
||||
docPath: string,
|
||||
setDocInfo: (info: DocumentInfo | null) => void,
|
||||
setLoading: (loading: boolean) => void,
|
||||
setTextContent: (text: string) => void
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const docUrl = docPath.startsWith('http') ? docPath : `${DOCUMENT_URL}${docPath}`;
|
||||
// console.log('[ComparePreview] Loading document:', docUrl);
|
||||
|
||||
const fileType = detectFileType(docPath);
|
||||
|
||||
if (fileType === 'pdf') {
|
||||
const pdfInfo = await detectPdfType(docUrl);
|
||||
const text = await extractTextFromPdf(docUrl);
|
||||
|
||||
const docInfo: DocumentInfo = {
|
||||
fileType: 'pdf',
|
||||
pdfType: pdfInfo.type,
|
||||
numPages: pdfInfo.numPages,
|
||||
textLength: pdfInfo.textLength,
|
||||
confidence: pdfInfo.confidence
|
||||
};
|
||||
|
||||
setDocInfo(docInfo);
|
||||
setTextContent(text);
|
||||
|
||||
// console.log('[ComparePreview] PDF text extracted:', {
|
||||
// path: docPath,
|
||||
// textLength: text.length,
|
||||
// firstChars: text.substring(0, 100)
|
||||
// });
|
||||
|
||||
if (pdfInfo.type === 'text') {
|
||||
toastService.success(`PDF加载成功!共 ${pdfInfo.numPages} 页,提取了 ${pdfInfo.textLength} 个字符`);
|
||||
} else if (pdfInfo.type === 'scanned') {
|
||||
toastService.warning('检测到扫描版PDF,文本提取质量可能较低');
|
||||
} else {
|
||||
toastService.error('无法识别PDF类型,可能是图片PDF');
|
||||
}
|
||||
} else if (fileType === 'docx') {
|
||||
const text = await extractTextFromWord(docUrl);
|
||||
|
||||
const docInfo: DocumentInfo = {
|
||||
fileType: 'docx',
|
||||
textLength: text.length,
|
||||
confidence: 1.0
|
||||
};
|
||||
|
||||
setDocInfo(docInfo);
|
||||
setTextContent(text);
|
||||
|
||||
// console.log('[ComparePreview] Word text extracted:', {
|
||||
// path: docPath,
|
||||
// textLength: text.length,
|
||||
// firstChars: text.substring(0, 100)
|
||||
// });
|
||||
|
||||
toastService.success(`Word文档加载成功!提取了 ${text.length} 个字符`);
|
||||
} else {
|
||||
toastService.error('不支持的文件类型');
|
||||
setTextContent('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Document loading failed:', error);
|
||||
toastService.error(`文档加载失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
setDocInfo(null);
|
||||
setTextContent('');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Monaco Editor mount callback
|
||||
const handleEditorDidMount = (editor: editor.IStandaloneDiffEditor) => {
|
||||
diffEditorRef.current = editor;
|
||||
|
||||
// console.log('[ComparePreview] Editor mounted, checking differences...', {
|
||||
// originalTextLength: originalText.length,
|
||||
// modifiedTextLength: modifiedText.length
|
||||
// });
|
||||
|
||||
// Polling function to get diff results
|
||||
// Monaco Editor's diff calculation is asynchronous and may return null initially
|
||||
let retryCount = 0;
|
||||
const maxRetries = 20; // Maximum 20 attempts
|
||||
const retryInterval = 150; // Check every 150ms
|
||||
|
||||
const pollForDiffChanges = () => {
|
||||
retryCount++;
|
||||
const lineChanges = editor.getLineChanges();
|
||||
|
||||
// console.log(`[ComparePreview] Polling attempt ${retryCount}:`, {
|
||||
// lineChanges: lineChanges ? `${lineChanges.length} changes` : 'null',
|
||||
// hasLineChanges: lineChanges !== null
|
||||
// });
|
||||
|
||||
// If we got the diff results
|
||||
if (lineChanges !== null) {
|
||||
setDiffCount(lineChanges.length);
|
||||
// console.log(`[ComparePreview] ✅ Successfully got ${lineChanges.length} differences on attempt ${retryCount}`);
|
||||
|
||||
// Verify the result makes sense
|
||||
if (lineChanges.length === 0 && originalText.length > 0 && modifiedText.length > 0) {
|
||||
const textsAreIdentical = originalText === modifiedText;
|
||||
// console.log('[ComparePreview] Texts are identical?', textsAreIdentical);
|
||||
|
||||
if (textsAreIdentical) {
|
||||
// console.log('[ComparePreview] ℹ️ Documents are identical - 0 differences is correct');
|
||||
} else {
|
||||
console.warn('[ComparePreview] ⚠️ Documents differ but Monaco shows 0 differences');
|
||||
// console.log('[ComparePreview] Original text sample:', originalText.substring(0, 100));
|
||||
// console.log('[ComparePreview] Modified text sample:', modifiedText.substring(0, 100));
|
||||
}
|
||||
}
|
||||
return; // Stop polling
|
||||
}
|
||||
|
||||
// If still null and haven't exceeded max retries, try again
|
||||
if (retryCount < maxRetries) {
|
||||
setTimeout(pollForDiffChanges, retryInterval);
|
||||
} else {
|
||||
console.error(`[ComparePreview] ❌ Failed to get diff results after ${maxRetries} attempts`);
|
||||
setDiffCount(0);
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling after a short delay
|
||||
setTimeout(pollForDiffChanges, 100);
|
||||
};
|
||||
|
||||
// Add highlight flash animation when navigating to a difference
|
||||
const addFlashHighlight = (editor: editor.ICodeEditor, lineNumber: number) => {
|
||||
const decorations = editor.deltaDecorations([], [
|
||||
{
|
||||
range: new (window as any).monaco.Range(lineNumber, 1, lineNumber, 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: 'diff-flash-highlight',
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
// Remove the highlight after animation completes
|
||||
setTimeout(() => {
|
||||
editor.deltaDecorations(decorations, []);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// Go to next difference
|
||||
const goToNextDiff = () => {
|
||||
if (!diffEditorRef.current) return;
|
||||
|
||||
const lineChanges = diffEditorRef.current.getLineChanges();
|
||||
if (!lineChanges || lineChanges.length === 0) return;
|
||||
|
||||
const nextIndex = (currentDiff + 1) % lineChanges.length;
|
||||
const nextChange = lineChanges[nextIndex];
|
||||
|
||||
const modifiedEditor = diffEditorRef.current.getModifiedEditor();
|
||||
modifiedEditor.revealLineInCenter(nextChange.modifiedStartLineNumber);
|
||||
modifiedEditor.setPosition({
|
||||
lineNumber: nextChange.modifiedStartLineNumber,
|
||||
column: 1
|
||||
});
|
||||
|
||||
// Add flash highlight
|
||||
addFlashHighlight(modifiedEditor, nextChange.modifiedStartLineNumber);
|
||||
|
||||
setCurrentDiff(nextIndex);
|
||||
};
|
||||
|
||||
// Go to previous difference
|
||||
const goToPreviousDiff = () => {
|
||||
if (!diffEditorRef.current) return;
|
||||
|
||||
const lineChanges = diffEditorRef.current.getLineChanges();
|
||||
if (!lineChanges || lineChanges.length === 0) return;
|
||||
|
||||
const prevIndex = currentDiff === 0 ? lineChanges.length - 1 : currentDiff - 1;
|
||||
const prevChange = lineChanges[prevIndex];
|
||||
|
||||
const modifiedEditor = diffEditorRef.current.getModifiedEditor();
|
||||
modifiedEditor.revealLineInCenter(prevChange.modifiedStartLineNumber);
|
||||
modifiedEditor.setPosition({
|
||||
lineNumber: prevChange.modifiedStartLineNumber,
|
||||
column: 1
|
||||
});
|
||||
|
||||
// Add flash highlight
|
||||
addFlashHighlight(modifiedEditor, prevChange.modifiedStartLineNumber);
|
||||
|
||||
setCurrentDiff(prevIndex);
|
||||
};
|
||||
|
||||
// Load documents on mount
|
||||
useEffect(() => {
|
||||
// console.log('[ComparePreview] Doc1 path changed:', doc1Path);
|
||||
if (doc1Path) {
|
||||
loadDocumentAndExtractText(doc1Path, setDoc1Info, setIsLoadingDoc1, setOriginalText);
|
||||
} else {
|
||||
setOriginalText('');
|
||||
}
|
||||
}, [doc1Path]);
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('[ComparePreview] Doc2 path changed:', doc2Path);
|
||||
if (doc2Path) {
|
||||
loadDocumentAndExtractText(doc2Path, setDoc2Info, setIsLoadingDoc2, setModifiedText);
|
||||
} else {
|
||||
setModifiedText('');
|
||||
}
|
||||
}, [doc2Path]);
|
||||
|
||||
// Recalculate differences when text changes
|
||||
useEffect(() => {
|
||||
// console.log('[ComparePreview] Text changed, recalculating differences...', {
|
||||
// originalTextLength: originalText.length,
|
||||
// modifiedTextLength: modifiedText.length,
|
||||
// hasEditor: !!diffEditorRef.current
|
||||
// });
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (diffEditorRef.current) {
|
||||
const lineChanges = diffEditorRef.current.getLineChanges();
|
||||
if (lineChanges) {
|
||||
setDiffCount(lineChanges.length);
|
||||
// console.log(`[ComparePreview] Recalculated differences: ${lineChanges.length} found`);
|
||||
|
||||
// 如果文本都不为空但差异为0,打印警告
|
||||
if (lineChanges.length === 0 && originalText.length > 0 && modifiedText.length > 0) {
|
||||
console.warn('[ComparePreview] Warning: Both texts loaded but 0 differences found!', {
|
||||
doc1PathsMatch: doc1Path === doc2Path,
|
||||
originalSample: originalText.substring(0, 50),
|
||||
modifiedSample: modifiedText.substring(0, 50)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [originalText, modifiedText, doc1Path, doc2Path]);
|
||||
|
||||
return (
|
||||
<div className="compare-preview-container" style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Character-level diff highlighting styles */}
|
||||
<style>{`
|
||||
.compare-preview-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-diff-editor .char-insert {
|
||||
background-color: rgba(100, 150, 50, 0.6) !important;
|
||||
}
|
||||
|
||||
.monaco-diff-editor .char-delete {
|
||||
background-color: rgba(200, 50, 50, 0.5) !important;
|
||||
}
|
||||
|
||||
.monaco-diff-editor .line-insert .char-insert {
|
||||
background-color: rgba(100, 150, 50, 0.7) !important;
|
||||
}
|
||||
|
||||
.monaco-diff-editor .line-delete .char-delete {
|
||||
background-color: rgba(200, 50, 50, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Flash highlight animation for navigation */
|
||||
.diff-flash-highlight {
|
||||
background-color: rgba(255, 200, 0, 0.3) !important;
|
||||
animation: flash-pulse 1s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes flash-pulse {
|
||||
0%, 100% { background-color: rgba(255, 200, 0, 0); }
|
||||
50% { background-color: rgba(255, 200, 0, 0.5); }
|
||||
}
|
||||
|
||||
/* Navigation button focus styles */
|
||||
.nav-diff-button {
|
||||
padding: 6px 12px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-diff-button:hover:not(:disabled) {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #00684a;
|
||||
}
|
||||
|
||||
.nav-diff-button:focus {
|
||||
outline: none;
|
||||
border-color: #00684a;
|
||||
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.2);
|
||||
}
|
||||
|
||||
.nav-diff-button:active:not(:disabled) {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-diff-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Toolbar and Info banner - combined in one row */}
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
backgroundColor: '#f5f5f5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '16px'
|
||||
}}>
|
||||
{/* Left side: Toolbar controls */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexShrink: 0 }}>
|
||||
{/* Diff statistics */}
|
||||
<div style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
color: '#333'
|
||||
}}>
|
||||
<i className="ri-git-compare-line" style={{ marginRight: '6px', color: '#00684a' }}></i>
|
||||
发现 <strong style={{ color: '#00684a' }}>{diffCount}</strong> 处差异
|
||||
{diffCount > 0 && ` (当前: ${currentDiff + 1}/${diffCount})`}
|
||||
</div>
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<button
|
||||
className="nav-diff-button"
|
||||
onClick={goToPreviousDiff}
|
||||
disabled={diffCount === 0}
|
||||
>
|
||||
<i className="ri-arrow-up-line"></i>
|
||||
上一处差异
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="nav-diff-button"
|
||||
onClick={goToNextDiff}
|
||||
disabled={diffCount === 0}
|
||||
>
|
||||
<i className="ri-arrow-down-line"></i>
|
||||
下一处差异
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right side: Info banner */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '6px 12px',
|
||||
backgroundColor: '#e7f3ff',
|
||||
border: '1px solid #b3d9ff',
|
||||
borderRadius: '4px',
|
||||
fontSize: '13px',
|
||||
color: '#004085',
|
||||
flexShrink: 1,
|
||||
minWidth: 0
|
||||
}}>
|
||||
<i className="ri-information-line" style={{ fontSize: '16px', flexShrink: 0 }}></i>
|
||||
<div style={{ lineHeight: '1.5', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
<strong>差异高亮说明:</strong>
|
||||
<span style={{ marginLeft: '8px' }}>
|
||||
<span style={{ color: '#dc3545', fontWeight: 'bold' }}>左侧红色</span>:原始版本 |
|
||||
<span style={{ color: '#28a745', fontWeight: 'bold', marginLeft: '8px' }}>右侧绿色</span>:修改版本 |
|
||||
<span style={{ color: '#666', fontWeight: 'bold', marginLeft: '8px' }}>深色高亮</span>:字符差异
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff Editor main area */}
|
||||
<div style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
|
||||
{/* 只有当两个文本都加载完成后才渲染 Monaco Editor */}
|
||||
{originalText && modifiedText && !isLoadingDoc1 && !isLoadingDoc2 ? (
|
||||
<DiffEditor
|
||||
key={`${doc1Path}-${doc2Path}-${originalText.length}-${modifiedText.length}`}
|
||||
height="100%"
|
||||
language="plaintext"
|
||||
original={originalText}
|
||||
modified={modifiedText}
|
||||
onMount={handleEditorDidMount}
|
||||
theme="vs"
|
||||
options={{
|
||||
readOnly: true,
|
||||
renderSideBySide: true,
|
||||
ignoreTrimWhitespace: false,
|
||||
renderWhitespace: 'selection',
|
||||
fontSize: 14,
|
||||
lineNumbers: 'on',
|
||||
minimap: {
|
||||
enabled: true
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
automaticLayout: true,
|
||||
renderIndicators: true,
|
||||
diffWordWrap: 'on',
|
||||
enableSplitViewResizing: true,
|
||||
diffAlgorithm: 'advanced',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#666',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
{isLoadingDoc1 || isLoadingDoc2 ? (
|
||||
<span>正在加载文档...</span>
|
||||
) : (
|
||||
<span>等待文档加载...</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading overlay */}
|
||||
{(isLoadingDoc1 || isLoadingDoc2) && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<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' }}>
|
||||
正在加载文档并提取文本...
|
||||
</div>
|
||||
{isLoadingDoc1 && <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>📄 加载原始文档</div>}
|
||||
{isLoadingDoc2 && <div style={{ fontSize: '14px', color: '#666', marginTop: '8px' }}>📄 加载对比文档</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Spinner animation */}
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user