200 lines
6.2 KiB
TypeScript
200 lines
6.2 KiB
TypeScript
/**
|
||
* 文件预览组件
|
||
* 显示文档内容和评查点高亮
|
||
*/
|
||
import { useState, useEffect, useRef } from 'react';
|
||
|
||
// 定义评查点类型
|
||
interface ReviewPoint {
|
||
id: string;
|
||
title: string;
|
||
status: string;
|
||
content: string;
|
||
suggestion: string;
|
||
position?: {
|
||
section: string;
|
||
index: number;
|
||
};
|
||
}
|
||
|
||
// 定义文档内容类型
|
||
interface FileContent {
|
||
title: string;
|
||
contractNumber: string;
|
||
parties: {
|
||
partyA: {
|
||
name: string;
|
||
address: string;
|
||
representative: string;
|
||
phone: string;
|
||
};
|
||
partyB: {
|
||
name: string;
|
||
address: string;
|
||
representative: string;
|
||
phone: string;
|
||
};
|
||
};
|
||
sections: {
|
||
title: string;
|
||
content: string;
|
||
}[];
|
||
}
|
||
|
||
interface FilePreviewProps {
|
||
fileContent: FileContent;
|
||
reviewPoints: ReviewPoint[];
|
||
activeReviewPointId: string | null;
|
||
}
|
||
|
||
export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }: FilePreviewProps) {
|
||
const [zoomLevel, setZoomLevel] = useState(100);
|
||
const [highlightsVisible, setHighlightsVisible] = useState(true);
|
||
const contentRef = useRef<HTMLDivElement>(null);
|
||
|
||
// 放大文档
|
||
const handleZoomIn = () => {
|
||
if (zoomLevel < 200) {
|
||
setZoomLevel(prevZoom => prevZoom + 10);
|
||
}
|
||
};
|
||
|
||
// 缩小文档
|
||
const handleZoomOut = () => {
|
||
if (zoomLevel > 50) {
|
||
setZoomLevel(prevZoom => prevZoom - 10);
|
||
}
|
||
};
|
||
|
||
// 切换高亮显示
|
||
const toggleHighlights = () => {
|
||
setHighlightsVisible(!highlightsVisible);
|
||
};
|
||
|
||
// 当选中的评查点变化时,滚动到对应位置
|
||
useEffect(() => {
|
||
if (activeReviewPointId && contentRef.current) {
|
||
const highlightElement = contentRef.current.querySelector(`[data-review-id="${activeReviewPointId}"]`);
|
||
if (highlightElement) {
|
||
highlightElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
|
||
// 添加临时突出显示效果
|
||
highlightElement.classList.add('highlight-focus');
|
||
setTimeout(() => {
|
||
highlightElement.classList.remove('highlight-focus');
|
||
}, 1500);
|
||
}
|
||
}
|
||
}, [activeReviewPointId]);
|
||
|
||
// 获取评查点对应的样式类
|
||
const getHighlightClass = (status: string) => {
|
||
switch (status) {
|
||
case 'warning':
|
||
return 'warning';
|
||
case 'error':
|
||
return 'error';
|
||
case 'success':
|
||
return 'success';
|
||
default:
|
||
return 'warning';
|
||
}
|
||
};
|
||
|
||
// 渲染文档内容
|
||
const renderDocumentContent = () => {
|
||
return (
|
||
<div className="word-document" ref={contentRef} style={{transform: `scale(${zoomLevel/100})`, transformOrigin: 'center top'}}>
|
||
<h1>{fileContent.title}</h1>
|
||
<p style={{textAlign: 'right'}}>合同编号:{fileContent.contractNumber}</p>
|
||
|
||
<div style={{margin: '20px 0'}}>
|
||
<p><strong>甲方(供方):</strong>{fileContent.parties.partyA.name}</p>
|
||
<p>地址:{fileContent.parties.partyA.address}</p>
|
||
<p>法定代表人:{fileContent.parties.partyA.representative}</p>
|
||
<p>联系电话:{fileContent.parties.partyA.phone}</p>
|
||
<p> </p>
|
||
<p><strong>乙方(需方):</strong>{fileContent.parties.partyB.name}</p>
|
||
<p>地址:{fileContent.parties.partyB.address}</p>
|
||
<p>法定代表人:{fileContent.parties.partyB.representative}</p>
|
||
<p>联系电话:{fileContent.parties.partyB.phone}</p>
|
||
</div>
|
||
|
||
<p>根据《中华人民共和国合同法》及有关法律法规的规定,经双方协商一致,签订本合同,共同遵守。</p>
|
||
|
||
{fileContent.sections.map((section, sectionIndex) => (
|
||
<div key={sectionIndex}>
|
||
<h2>{section.title}</h2>
|
||
{renderSectionContent(section.content, section.title, sectionIndex)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 渲染章节内容,处理高亮
|
||
const renderSectionContent = (content: string, sectionTitle: string, sectionIndex: number) => {
|
||
const lines = content.split('\n');
|
||
|
||
return lines.map((line, lineIndex) => {
|
||
// 查找该行对应的评查点
|
||
const reviewPoint = reviewPoints.find(point =>
|
||
point.position &&
|
||
point.position.section === sectionTitle &&
|
||
point.position.index === lineIndex
|
||
);
|
||
|
||
if (reviewPoint && highlightsVisible) {
|
||
// 如果有对应的评查点,添加高亮
|
||
const isActive = reviewPoint.id === activeReviewPointId;
|
||
return (
|
||
<div
|
||
key={`${sectionIndex}-${lineIndex}`}
|
||
className={`highlight-area ${getHighlightClass(reviewPoint.status)} ${isActive ? 'highlight-focus' : ''}`}
|
||
data-review-id={reviewPoint.id}
|
||
style={isActive ? {zIndex: 20, boxShadow: '0 0 0 2px yellow, 0 0 10px rgba(0,0,0,0.3)'} : {}}
|
||
>
|
||
<p>{line}</p>
|
||
</div>
|
||
);
|
||
} else {
|
||
// 没有评查点,正常显示
|
||
return <p key={`${sectionIndex}-${lineIndex}`}>{line}</p>;
|
||
}
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="file-preview">
|
||
<div className="file-preview-header py-2 px-4">
|
||
<div className="flex items-center">
|
||
<i className="ri-file-text-line text-primary mr-2"></i>
|
||
<span className="font-medium text-primary">文件预览</span>
|
||
</div>
|
||
<div className="file-preview-actions">
|
||
<button
|
||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
|
||
onClick={handleZoomIn}
|
||
>
|
||
<i className="ri-zoom-in-line"></i>
|
||
</button>
|
||
<button
|
||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
|
||
onClick={handleZoomOut}
|
||
>
|
||
<i className="ri-zoom-out-line"></i>
|
||
</button>
|
||
<button
|
||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs h-5 leading-5"
|
||
onClick={toggleHighlights}
|
||
>
|
||
<i className="ri-mark-pen-line"></i> {highlightsVisible ? '隐藏问题' : '显示问题'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="file-preview-content">
|
||
{renderDocumentContent()}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|