fix: 1.接入ai_suggestion.

2. 接入合同起草功能。
This commit is contained in:
2025-12-05 00:04:45 +08:00
parent eca98fc540
commit 33f10896a0
29 changed files with 3184 additions and 981 deletions
+129 -67
View File
@@ -2,7 +2,7 @@
* 文件预览组件
* 显示文档内容和评查点高亮
*/
import { useState, useEffect, useRef, ChangeEvent } from 'react';
import { useState, useEffect, useRef, forwardRef, useImperativeHandle, ChangeEvent } from 'react';
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
import { requestPageInfo, customGotoPage } from '~/components/collabora/lib';
import { PdfPreview } from './previewComponents/PdfPreview';
@@ -64,14 +64,22 @@ interface FilePreviewProps {
replaceText: string;
pageNumber: number;
}; // AI建议替换参数
isTemplate?: boolean;
}
// 暴露给父组件的接口
export interface FilePreviewHandle {
collaboraViewerRef: React.RefObject<CollaboraViewerHandle>;
}
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace }: FilePreviewProps) {
export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) {
// 获取文件类型
const real_path = fileContent.path || fileContent.template_contract_path || '';
const fileExtension = real_path.split('.').pop()?.toLowerCase();
// 是文档类型 并且 是模板文件
const isDocx = fileExtension === 'docx';
// 是模板文件 或 是pdf文件就用pdf渲染
const isPdf = fileExtension === 'pdf';
// ✅ 将所有hooks移到条件return之前,确保遵守React Hooks规则
@@ -80,6 +88,11 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
const collaboraViewerRef = useRef<CollaboraViewerHandle>(null);
const prevTargetPageRef = useRef<number | undefined>(undefined);
// 暴露 collaboraViewerRef 给父组件
useImperativeHandle(ref, () => ({
collaboraViewerRef
}));
// States
const [numPages, setNumPages] = useState<number | null>(null);
const [pageInputValue, setPageInputValue] = useState<string>('');
@@ -265,17 +278,20 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
return;
}
// if (targetPageNum > 0 && (!numPages || targetPageNum <= numPages)) {
if (targetPageNum > 0) {
try {
await customGotoPage(iframeWindow, targetPageNum);
setPageInputValue('');
// toastService.success(`已跳转至第 ${targetPageNum} 页`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
toastService.error(`跳转失败: ${errorMessage}`);
}
// } else if (numPages && targetPageNum > numPages) {
// toastService.warning(`页码不能超过总页数 ${numPages}`);
} else {
toastService.warning('请输入有效页码');
setPageInputValue('');
}
};
@@ -286,17 +302,55 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
}
};
// 滚动到顶部(仅用于 DOCX
// 滚动到顶部(支持 PDF 和 DOCX
const handleScrollToTop = async () => {
if (!collaboraViewerRef.current?.isReady) {
toastService.warning('文档尚未加载完成,请稍候...');
return;
}
setIsScrollingToTop(true);
try {
await collaboraViewerRef.current?.unoCommands.scrollToTop();
console.log('[FilePreview] 已返回顶部');
if (isPdf) {
// PDF文件:滚动到第一个页面元素
const firstPage = document.querySelector('[data-page-number="1"]');
if (firstPage) {
firstPage.scrollIntoView({ behavior: 'smooth', block: 'start' });
// console.log('[FilePreview] PDF已返回顶部');
} else if (contentRef.current) {
// 如果找不到页面元素,则滚动容器到顶部
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
// console.log('[FilePreview] PDF容器已返回顶部');
}
} else if (isDocx) {
// DOCX文件:模板预览模式尝试多种滚动方式,编辑模式使用Collabora命令
if (isTemplate) {
// 模板预览模式:尝试多种滚动方式
console.log('[FilePreview] 尝试返回顶部...');
// 1. 尝试滚动 contentRef 容器
if (contentRef.current) {
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
console.log('[FilePreview] 已滚动 contentRef 容器');
}
// 2. 尝试滚动外层的 file-preview-isolation 容器
const isolationContainer = document.querySelector('.file-preview-isolation');
if (isolationContainer) {
isolationContainer.scrollTo({ top: 0, behavior: 'smooth' });
console.log('[FilePreview] 已滚动 isolation 容器');
}
// 3. 最后滚动整个窗口
window.scrollTo({ top: 0, behavior: 'smooth' });
console.log('[FilePreview] 已滚动窗口');
} else {
// 编辑模式:使用Collabora UNO命令
if (!collaboraViewerRef.current?.isReady) {
toastService.warning('文档尚未加载完成,请稍候...');
setIsScrollingToTop(false);
return;
}
await collaboraViewerRef.current?.unoCommands.scrollToTop();
console.log('[FilePreview] DOCX已返回顶部(UNO命令)');
}
}
} catch (error) {
console.error('[FilePreview] 返回顶部失败:', error);
toastService.error('返回顶部失败');
@@ -357,11 +411,13 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
const highlightText = highlightValue;
// DOCX文件使用Collabora Online预览
// 如果是模板预览,使用只读模式;否则使用编辑模式
return (
<CollaboraViewer
ref={collaboraViewerRef}
fileId={real_path}
mode="edit"
mode={isTemplate ? "view" : "edit"}
// mode={"edit"}
userId={userInfo?.sub || 'guest'}
userName={userInfo?.nick_name || ''}
targetPage={targetPage}
@@ -380,8 +436,8 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
};
return (
<div className="file-preview">
<div className="file-preview-header px-2 text-xs sm:text-xs md:text-sm max-w-full flex items-center justify-between min-w-0">
<div className="file-preview h-full flex flex-col">
<div className="file-preview-header px-2 text-xs sm:text-xs md:text-sm max-w-full flex items-center justify-between min-w-0 flex-shrink-0">
<div className="flex items-center min-w-0 flex-shrink-0">
<i className={`${isStructuredView ? 'ri-file-list-line' : 'ri-file-text-line'} text-primary mr-2 flex-shrink-0`}></i>
<span className="font-medium text-primary truncate max-w-[120px]" title={isStructuredView ? '模板预览' : '文件预览'}>
@@ -389,27 +445,30 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
</span>
</div>
<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="返回顶部"
>
{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 && (
{/* 返回顶部按钮 - 不在模板预览时显示 */}
{!isTemplate && (
<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="返回顶部"
>
{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 && !isTemplate && (
<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
@@ -430,31 +489,39 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
</span>
</button>
)}
{/* 页码跳转控件 */}
<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="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 || isDocumentLoading}
title="跳转到页面"
>
<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">
/ {numPages}
</span>
)}
</div>
{/* 页码跳转控件 - 不在模板预览时显示 */}
{!isTemplate && (
<div className="inline-flex items-center bg-white border border-gray-300 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 flex-shrink-0 overflow-hidden">
<div className="flex items-center px-2 py-1">
<i className="ri-file-list-line text-sm text-gray-400 mr-1.5"></i>
<input
type="text"
className="w-10 h-5 px-1 text-xs text-center text-gray-700 bg-transparent border-0 outline-none focus:text-primary transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="页码"
value={pageInputValue}
onChange={handlePageInputChange}
onKeyDown={handlePageInputKeyDown}
disabled={isDocumentLoading}
/>
{/* {numPages && (
<span className="text-xs text-gray-400 mx-0.5">/</span>
)}
{numPages && (
<span className="text-xs text-gray-500 font-medium min-w-[1.5rem] text-center">
{numPages}
</span>
)} */}
</div>
<button
className="flex items-center justify-center h-7 px-2.5 text-white bg-primary hover:bg-primary-hover transition-colors duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary border-l border-primary-hover/20"
onClick={handlePageJump}
disabled={!numPages || isDocumentLoading}
title="跳转"
>
<i className="ri-skip-forward-mini-line text-sm"></i>
</button>
</div>
)}
{/* 缩放提示 - 仅在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">
@@ -465,13 +532,8 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
</div>
</div>
<div
className="file-preview-content"
className="file-preview-content flex-1 overflow-auto"
ref={contentRef}
style={{
maxHeight: 'calc(100vh - 150px)',
overflowY: 'auto',
overflowX: 'auto',
}}
>
<div
className="pdf-interactive-container"
@@ -489,4 +551,4 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
</div>
</div>
);
}
});