feat: 1. 接入CollaboraViewer选中的高亮效果,清除高亮功能,页面销毁自动清除高亮。
2. 合同模板对比接入monaco editor的效果。 3. 添加交叉评查的案卷类型的数据查询。 fix: 1. 修复文档列表的打开模态框蒙板层显示效果。
This commit is contained in:
@@ -52,7 +52,8 @@ interface FilePreviewProps {
|
||||
reviewPoints?: ReviewPoint[]; // 设为可选
|
||||
activeReviewPointResultId: string | null;
|
||||
targetPage?: number; // 新增目标页码参数
|
||||
charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息
|
||||
charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息(仅用于PDF)
|
||||
highlightValue?: string; // 高亮文本值(用于DOCX)
|
||||
isStructuredView?: boolean; // 是否显示结构化视图
|
||||
userInfo?: {
|
||||
sub: string;
|
||||
@@ -61,7 +62,7 @@ interface FilePreviewProps {
|
||||
}
|
||||
|
||||
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||||
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, isStructuredView = false, userInfo }: FilePreviewProps) {
|
||||
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo }: FilePreviewProps) {
|
||||
// 获取文件类型
|
||||
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
||||
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
||||
@@ -73,16 +74,51 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const collaboraViewerRef = useRef<CollaboraViewerHandle>(null);
|
||||
const prevTargetPageRef = useRef<number | undefined>(undefined);
|
||||
const lastMousePosRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
// States
|
||||
const [numPages, setNumPages] = useState<number | null>(null);
|
||||
const [pageInputValue, setPageInputValue] = useState<string>('');
|
||||
const [dragMode, setDragMode] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragCursor, setDragCursor] = useState('default');
|
||||
const [isDocumentLoading, setIsDocumentLoading] = useState<boolean>(true); // 文档加载状态
|
||||
const [isScrollingToTop, setIsScrollingToTop] = useState<boolean>(false); // 返回顶部loading状态
|
||||
const [isClearingHighlights, setIsClearingHighlights] = useState<boolean>(false); // 清除高亮loading状态
|
||||
|
||||
// ✅ 将所有useEffect移到条件return之前
|
||||
// 清除高亮:在组件卸载或文档路径变化时
|
||||
useEffect(() => {
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (isDocx && collaboraViewerRef.current?.isReady) {
|
||||
console.log('[FilePreview] 🔥 文档切换,调用 clearAllHighlights');
|
||||
// 调用暴露的清除方法
|
||||
collaboraViewerRef.current.clearAllHighlights().catch(error => {
|
||||
console.error('[FilePreview] ✗ 清除高亮失败:', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [real_path, isDocx]); // 当文档路径变化时,清除旧文档的高亮
|
||||
|
||||
// 监听文档加载状态
|
||||
useEffect(() => {
|
||||
if (!isDocx) {
|
||||
setIsDocumentLoading(false); // 非DOCX文件直接设为已加载
|
||||
return;
|
||||
}
|
||||
|
||||
// DOCX文件需要等待 Collabora 准备就绪
|
||||
setIsDocumentLoading(true);
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
if (collaboraViewerRef.current?.isReady) {
|
||||
setIsDocumentLoading(false);
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
clearInterval(checkInterval);
|
||||
};
|
||||
}, [isDocx, real_path]); // 当文档路径变化时重新检测
|
||||
|
||||
// DOCX 页数获取: 使用 requestPageInfo 方法
|
||||
useEffect(() => {
|
||||
if (!isDocx || isPdf) return; // PDF文件不需要执行
|
||||
@@ -123,31 +159,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
};
|
||||
}, [isDocx, isPdf]);
|
||||
|
||||
// 监听鼠标离开窗口事件
|
||||
useEffect(() => {
|
||||
if (isPdf) return; // PDF不需要拖拽功能
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (dragMode && isDragging) {
|
||||
setIsDragging(false);
|
||||
setDragCursor('grab');
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!dragMode) return;
|
||||
setIsDragging(false);
|
||||
setDragCursor('grab');
|
||||
};
|
||||
|
||||
document.addEventListener('mouseleave', handleMouseLeave);
|
||||
document.addEventListener('mouseup', handleMouseUp as EventListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseleave', handleMouseLeave);
|
||||
document.removeEventListener('mouseup', handleMouseUp as EventListener);
|
||||
};
|
||||
}, [isDragging, dragMode, isPdf]);
|
||||
|
||||
// 处理页面跳转
|
||||
useEffect(() => {
|
||||
@@ -214,75 +225,6 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
|
||||
// DOCX 和其他文件类型继续使用原有逻辑
|
||||
|
||||
// 放大文档(仅用于 DOCX)
|
||||
const handleZoomIn = () => {
|
||||
if (!collaboraViewerRef.current?.isReady) {
|
||||
toastService.warning('文档尚未加载完成,请稍候...');
|
||||
return;
|
||||
}
|
||||
collaboraViewerRef.current?.unoCommands.zoomIn();
|
||||
};
|
||||
|
||||
// 缩小文档(仅用于 DOCX)
|
||||
const handleZoomOut = () => {
|
||||
if (!collaboraViewerRef.current?.isReady) {
|
||||
toastService.warning('文档尚未加载完成,请稍候...');
|
||||
return;
|
||||
}
|
||||
collaboraViewerRef.current?.unoCommands.zoomOut();
|
||||
};
|
||||
|
||||
// 切换拖拽模式
|
||||
const toggleDragMode = () => {
|
||||
setDragMode(prev => !prev);
|
||||
setDragCursor(prev => prev === 'default' ? 'grab' : 'default');
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
// 处理拖拽开始
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!dragMode || e.button !== 0) return; // 只在拖拽模式下响应左键点击
|
||||
|
||||
// 防止选中文本
|
||||
e.preventDefault();
|
||||
|
||||
// 设置拖拽状态
|
||||
setIsDragging(true);
|
||||
setDragCursor('grabbing');
|
||||
|
||||
// 记录鼠标初始位置
|
||||
lastMousePosRef.current = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
};
|
||||
|
||||
// 处理拖拽过程
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!dragMode || !isDragging || !contentRef.current) return;
|
||||
|
||||
// 计算鼠标移动距离
|
||||
const dx = e.clientX - lastMousePosRef.current.x;
|
||||
const dy = e.clientY - lastMousePosRef.current.y;
|
||||
|
||||
// 更新容器滚动位置
|
||||
contentRef.current.scrollLeft -= dx;
|
||||
contentRef.current.scrollTop -= dy;
|
||||
|
||||
// 更新鼠标位置记录
|
||||
lastMousePosRef.current = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
};
|
||||
|
||||
// 处理拖拽结束
|
||||
const handleMouseUp = () => {
|
||||
if (!dragMode) return;
|
||||
|
||||
setIsDragging(false);
|
||||
setDragCursor('grab');
|
||||
};
|
||||
|
||||
// 获取评查点对应的样式类
|
||||
// const getHighlightClass = (status: string) => {
|
||||
@@ -339,12 +281,48 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
};
|
||||
|
||||
// 滚动到顶部(仅用于 DOCX)
|
||||
const handleScrollToTop = () => {
|
||||
const handleScrollToTop = async () => {
|
||||
if (!collaboraViewerRef.current?.isReady) {
|
||||
toastService.warning('文档尚未加载完成,请稍候...');
|
||||
return;
|
||||
}
|
||||
collaboraViewerRef.current?.unoCommands.scrollToTop();
|
||||
|
||||
setIsScrollingToTop(true);
|
||||
try {
|
||||
await collaboraViewerRef.current?.unoCommands.scrollToTop();
|
||||
console.log('[FilePreview] 已返回顶部');
|
||||
} catch (error) {
|
||||
console.error('[FilePreview] 返回顶部失败:', error);
|
||||
toastService.error('返回顶部失败');
|
||||
} finally {
|
||||
// 延迟500ms后重置loading状态,给用户足够的视觉反馈
|
||||
setTimeout(() => {
|
||||
setIsScrollingToTop(false);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 清除所有高亮(仅用于 DOCX)
|
||||
const handleClearAllHighlights = async () => {
|
||||
if (!collaboraViewerRef.current?.isReady) {
|
||||
toastService.warning('文档尚未加载完成,请稍候...');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsClearingHighlights(true);
|
||||
try {
|
||||
await collaboraViewerRef.current.clearAllHighlights();
|
||||
console.log('[FilePreview] 已清除所有高亮');
|
||||
toastService.success('已清除所有高亮');
|
||||
} catch (error) {
|
||||
console.error('[FilePreview] 清除高亮失败:', error);
|
||||
toastService.error('清除高亮失败');
|
||||
} finally {
|
||||
// 延迟500ms后重置loading状态
|
||||
setTimeout(() => {
|
||||
setIsClearingHighlights(false);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染文档内容
|
||||
@@ -368,6 +346,10 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
// 根据文件类型选择不同的渲染方式
|
||||
// 注意:PDF 文件已在组件开头使用 PdfPreview 组件提前返回
|
||||
if (fileExtension === 'docx') {
|
||||
// 使用 highlightValue 作为高亮文本(用户点击评查点时传递的实际文本值)
|
||||
// 不再从 charPositions 提取,因为 charPositions 是 PDF 特有的坐标信息
|
||||
const highlightText = highlightValue;
|
||||
|
||||
// DOCX文件使用Collabora Online预览
|
||||
return (
|
||||
<CollaboraViewer
|
||||
@@ -376,6 +358,8 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
mode="edit"
|
||||
userId={userInfo?.sub || 'guest'}
|
||||
userName={userInfo?.nick_name || ''}
|
||||
targetPage={targetPage}
|
||||
highlightText={highlightText}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -397,47 +381,66 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
{isStructuredView ? '模板预览' : '文件预览'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="file-preview-actions flex items-center ml-2 min-w-0 flex-1 justify-end overflow-hidden">
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 flex-shrink-0"
|
||||
<div className="file-preview-actions flex items-center ml-2 min-w-0 flex-1 justify-end overflow-hidden gap-2">
|
||||
<button
|
||||
className={`flex items-center justify-center px-2 py-1 text-xs text-gray-700 bg-white border border-gray-300 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
||||
isScrollingToTop || isDocumentLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-50 hover:border-primary hover:text-primary'
|
||||
}`}
|
||||
onClick={handleScrollToTop}
|
||||
disabled={isScrollingToTop || isDocumentLoading}
|
||||
title="返回顶部"
|
||||
>
|
||||
<i className="ri-arrow-up-double-line"></i>
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline truncate max-w-[60px]">返回顶部</span>
|
||||
</button>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 flex-shrink-0"
|
||||
onClick={handleZoomIn}
|
||||
title="放大"
|
||||
>
|
||||
<i className="ri-zoom-in-line"></i>
|
||||
</button>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 flex-shrink-0"
|
||||
onClick={handleZoomOut}
|
||||
title="缩小"
|
||||
>
|
||||
<i className="ri-zoom-out-line"></i>
|
||||
{isScrollingToTop ? (
|
||||
<i className="ri-loader-4-line text-sm animate-spin"></i>
|
||||
) : (
|
||||
<i className="ri-arrow-up-double-line text-sm"></i>
|
||||
)}
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||||
{isScrollingToTop ? '返回中...' : '返回顶部'}
|
||||
</span>
|
||||
</button>
|
||||
{/* 清除高亮按钮 - 仅在DOCX文档时显示 */}
|
||||
{isDocx && (
|
||||
<button
|
||||
className={`flex items-center justify-center px-2 py-1 text-xs text-white bg-red-500 border border-red-500 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
||||
isClearingHighlights || isDocumentLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-red-600 hover:border-red-600'
|
||||
}`}
|
||||
onClick={handleClearAllHighlights}
|
||||
disabled={isClearingHighlights || isDocumentLoading}
|
||||
title="清除所有高亮"
|
||||
>
|
||||
{isClearingHighlights ? (
|
||||
<i className="ri-loader-4-line text-sm animate-spin"></i>
|
||||
) : (
|
||||
<i className="ri-eraser-line text-sm"></i>
|
||||
)}
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||||
{isClearingHighlights ? '清除中...' : '清除高亮'}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{/* 页码跳转控件 */}
|
||||
<div className="inline-flex items-center ml-2 flex-shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
className="ant-input ant-input-sm py-0 px-1 text-xs max-h-6 leading-5 w-[2.5rem] text-center
|
||||
focus:outline-none focus:ring-1 focus:ring-green-900"
|
||||
<div className="inline-flex items-center flex-shrink-0 gap-1">
|
||||
<input
|
||||
type="text"
|
||||
className="w-12 h-7 px-2 text-xs text-center text-gray-700 bg-white border border-gray-300 rounded outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="页码"
|
||||
value={pageInputValue}
|
||||
onChange={handlePageInputChange}
|
||||
onKeyDown={handlePageInputKeyDown}
|
||||
disabled={isDocumentLoading}
|
||||
/>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-1"
|
||||
<button
|
||||
className="flex items-center justify-center w-7 h-7 text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:border-primary hover:text-primary transition-colors duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white disabled:hover:border-gray-300 disabled:hover:text-gray-700"
|
||||
onClick={handlePageJump}
|
||||
disabled={!numPages}
|
||||
disabled={!numPages || isDocumentLoading}
|
||||
title="跳转到页面"
|
||||
>
|
||||
<i className="ri-arrow-right-line"></i>
|
||||
<i className="ri-arrow-right-line text-sm"></i>
|
||||
</button>
|
||||
{numPages && (
|
||||
<span className="ml-1 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||||
@@ -445,66 +448,37 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 flex-shrink-0 ${dragMode ? 'active bg-green-300' : ''}`}
|
||||
title="切换拖拽模式"
|
||||
aria-pressed={dragMode}
|
||||
onClick={toggleDragMode}
|
||||
>
|
||||
<i className="ri-drag-move-line"></i>
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline truncate max-w-[80px]">
|
||||
拖拽模式{dragMode ? '(已激活)' : ''}
|
||||
</span>
|
||||
</button>
|
||||
{/* 缩放提示 - 仅在DOCX文档时显示 */}
|
||||
{isDocx && (
|
||||
<div className="flex items-center px-2 py-1 text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded flex-shrink-0">
|
||||
<i className="ri-zoom-in-line text-sm mr-1"></i>
|
||||
<span className="whitespace-nowrap">Ctrl+滚轮</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<div
|
||||
className="file-preview-content"
|
||||
ref={contentRef}
|
||||
style={{
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 150px)',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'auto',
|
||||
cursor: dragCursor,
|
||||
userSelect: dragMode ? 'none' : 'auto', // 拖拽模式下防止文本被选中
|
||||
}}
|
||||
>
|
||||
<button
|
||||
<div
|
||||
className="pdf-interactive-container"
|
||||
aria-label="PDF文档查看区域"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onKeyDown={(e) => {
|
||||
// 添加键盘导航支持
|
||||
const scrollAmount = 50;
|
||||
if (e.key === 'ArrowUp') {
|
||||
if (contentRef.current) contentRef.current.scrollTop -= scrollAmount;
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
if (contentRef.current) contentRef.current.scrollTop += scrollAmount;
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
if (contentRef.current) contentRef.current.scrollLeft -= scrollAmount;
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
if (contentRef.current) contentRef.current.scrollLeft += scrollAmount;
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'block',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
textAlign: 'center',
|
||||
padding: 0
|
||||
}}
|
||||
>
|
||||
{renderDocumentContent()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user