From d88cfc818b40ecdfca460af5dd5d879b5516f500 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Wed, 3 Dec 2025 12:07:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=201.=20=E5=AE=9E=E7=8E=B0=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E6=9B=BF=E6=8D=A2=E3=80=82=202.=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E9=99=84=E4=BB=B6=E5=92=8C=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=9A=84=E6=A0=B7=E5=BC=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/files/files-upload.ts | 4 +- app/components/collabora/CollaboraViewer.tsx | 309 ++++++++++++++---- app/components/collabora/types.ts | 6 + .../cross-checking/ReviewPointsList.tsx | 2 + app/components/reviews/FilePreview.tsx | 8 +- app/components/reviews/ReviewPointsList.tsx | 97 +++++- app/components/reviews/ReviewTabs.tsx | 4 +- app/components/rules/new/ReviewSettings.tsx | 15 +- app/routes/cross-checking.result.tsx | 21 ++ app/routes/documents.list.tsx | 49 ++- app/routes/files.upload.tsx | 162 +++++++-- app/routes/monaco-demo.tsx | 65 +++- app/routes/reviews.tsx | 26 +- 13 files changed, 627 insertions(+), 141 deletions(-) diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index 7919109..b457338 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -581,7 +581,7 @@ export async function getDocumentTypes(token?: string): Promise<{data: DocumentT * @returns 文档状态列表 */ export async function getDocumentsStatus( - documentIds: number[], + documentIds: number[], attachmentIds?: number[], token?: string ): Promise<{data: Document[]; error?: never} | {data?: never; error: string; status?: number}> { @@ -650,7 +650,7 @@ export async function getDocumentsStatus( return { data: allData }; } catch (error) { console.error('获取文档状态失败:', error); - return { + return { error: error instanceof Error ? error.message : '获取文档状态失败', status: 500 }; diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index 7af2a86..a387e75 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -43,6 +43,7 @@ export const CollaboraViewer = forwardRef(null); + // 搜索替换面板显示状态 + const [showSearchReplacePanel, setShowSearchReplacePanel] = useState(false); + // 标记是否应该自动执行搜索 + const shouldAutoSearchRef = useRef(false); + // 高亮测试面板状态 const [highlightTextInput, setHighlightTextInput] = useState(''); const [highlightPage, setHighlightPage] = useState(''); @@ -79,6 +85,12 @@ export const CollaboraViewer = forwardRef(null); const [replaceAllMode, setReplaceAllMode] = useState(false); + const [searchReplacePageNumber, setSearchReplacePageNumber] = useState(''); + // 记录上一次搜索的参数,用于判断是否需要重新跳转页面 + const [lastSearchParams, setLastSearchParams] = useState<{ + text: string; + page: string; + }>({ text: '', page: '' }); // 页面定点替换状态 const [replaceInPageNumber, setReplaceInPageNumber] = useState(''); @@ -97,18 +109,18 @@ export const CollaboraViewer = forwardRef { if (isDocumentLoaded && iframeRef.current?.contentWindow) { iframeWindowRef.current = iframeRef.current.contentWindow; - console.log('[CollaboraViewer] 已保存 iframe window 引用'); + // console.log('[CollaboraViewer] 已保存 iframe window 引用'); // 🔥 文档加载完成后主动清除一次高亮(防止缓存的高亮状态) - console.log('[CollaboraViewer] 🧹 文档加载完成,清除可能存在的缓存高亮'); + // console.log('[CollaboraViewer] 🧹 文档加载完成,清除可能存在的缓存高亮'); clearHighlights(iframeRef.current.contentWindow, { color: 16776960, timeout: 5000, }).then((result) => { if (result.count && result.count > 0) { - console.log(`[CollaboraViewer] ✓ 清除了 ${result.count} 个缓存的高亮区域`); + // console.log(`[CollaboraViewer] ✓ 清除了 ${result.count} 个缓存的高亮区域`); } else { - console.log('[CollaboraViewer] ✓ 文档无缓存高亮,已确认干净'); + // console.log('[CollaboraViewer] ✓ 文档无缓存高亮,已确认干净'); } }).catch(error => { console.warn('[CollaboraViewer] ⚠️ 清除缓存高亮失败:', error); @@ -128,12 +140,12 @@ export const CollaboraViewer = forwardRef { const savedWindow = iframeWindowRef.current || iframeRef.current?.contentWindow; if (savedWindow) { - console.log('[CollaboraViewer] 🧹 父组件调用清除高亮'); + // console.log('[CollaboraViewer] 🧹 父组件调用清除高亮'); await clearHighlights(savedWindow, { color: 16776960, timeout: 5000, }); - console.log('[CollaboraViewer] ✓ 清除高亮完成'); + // console.log('[CollaboraViewer] ✓ 清除高亮完成'); } else { console.warn('[CollaboraViewer] ⚠️ 无法清除高亮:iframe window 不可用'); } @@ -155,7 +167,7 @@ export const CollaboraViewer = forwardRef { + if (!aiSuggestionReplace || !isDocumentLoaded) { + return; + } + + console.log('[CollaboraViewer] 收到 AI 建议替换参数:', aiSuggestionReplace); + + const { searchText: newSearchText, replaceText: newReplaceText, pageNumber } = aiSuggestionReplace; + + // 显示搜索替换面板 + setShowSearchReplacePanel(true); + + // 设置搜索、替换和页码输入框的值 + setSearchText(newSearchText); + setReplaceText(newReplaceText); + setSearchReplacePageNumber(String(pageNumber)); + + // 设置自动搜索标志 + shouldAutoSearchRef.current = true; + + console.log('[CollaboraViewer] 已设置搜索参数,等待状态更新后自动执行查找'); + }, [aiSuggestionReplace, isDocumentLoaded]); + + // 8. 当搜索参数更新完成后,自动执行查找 + useEffect(() => { + if (shouldAutoSearchRef.current && searchText && searchReplacePageNumber && isDocumentLoaded) { + console.log('[CollaboraViewer] 状态更新完成,执行自动查找:', { searchText, searchReplacePageNumber }); + + // 重置标志 + shouldAutoSearchRef.current = false; + + // 延迟执行,确保 DOM 更新完成 + const timer = setTimeout(() => { + // console.log('[CollaboraViewer] 开始执行自动查找操作'); + handleSearchNext(); + }, 100); + + return () => clearTimeout(timer); + } + }, [searchText, searchReplacePageNumber, isDocumentLoaded]); // eslint-disable-line react-hooks/exhaustive-deps + // 加载中状态 if (loading) { return ( @@ -270,7 +324,7 @@ export const CollaboraViewer = forwardRef { if (!iframeRef.current?.contentWindow) { - setHighlightResult('iframe 未就绪'); + // setHighlightResult('iframe 未就绪'); return; } @@ -316,7 +370,7 @@ export const CollaboraViewer = forwardRef { if (!iframeRef.current?.contentWindow) { - setPageInfoResult('iframe 未就绪'); + // setPageInfoResult('iframe 未就绪'); return; } @@ -359,7 +413,7 @@ export const CollaboraViewer = forwardRef { if (!iframeRef.current?.contentWindow) { - setGotoPageResult('iframe 未就绪'); + // setGotoPageResult('iframe 未就绪'); return; } @@ -370,7 +424,7 @@ export const CollaboraViewer = forwardRef { @@ -402,7 +456,7 @@ export const CollaboraViewer = forwardRef { if (!iframeRef.current?.contentWindow) { - setClearHighlightResult('iframe 未就绪'); + // setClearHighlightResult('iframe 未就绪'); return; } @@ -417,7 +471,7 @@ export const CollaboraViewer = forwardRef { + const handleSearchNext = async () => { if (!iframeRef.current?.contentWindow) { - setSearchReplaceResult('iframe 未就绪'); + // setSearchReplaceResult('iframe 未就绪'); return; } @@ -456,8 +510,48 @@ export const CollaboraViewer = forwardRef setTimeout(resolve, 300)); + } catch (e) { + console.error('跳转页面失败:', e); + setSearchReplaceResult(`✗ 跳转失败: ${e instanceof Error ? e.message : '未知错误'}`); + return; + } + } + + // 执行搜索(如果是页面内循环查找,会自动找到下一个匹配项) + unoSearchNext(iframeRef.current.contentWindow, currentSearchText); + + // 更新搜索参数记录 + setLastSearchParams({ + text: currentSearchText, + page: currentPageNumber + }); + + // const pageInfo = currentPageNumber !== '' + // ? ` (第${currentPageNumber}页内循环查找)` + // : ''; + // const jumpInfo = needsPageJump && currentPageNumber !== '' ? ' [已跳转]' : ''; + // setSearchReplaceResult(`✓ 搜索: "${currentSearchText}"${pageInfo}${jumpInfo}`); // 3秒后清除提示 setTimeout(() => setSearchReplaceResult(null), 3000); @@ -468,9 +562,9 @@ export const CollaboraViewer = forwardRef { + const handleReplace = async () => { if (!iframeRef.current?.contentWindow) { - setSearchReplaceResult('iframe 未就绪'); + // setSearchReplaceResult('iframe 未就绪'); return; } @@ -480,12 +574,50 @@ export const CollaboraViewer = forwardRef setTimeout(resolve, 300)); + } catch (e) { + console.error('跳转页面失败:', e); + setSearchReplaceResult(`✗ 跳转失败: ${e instanceof Error ? e.message : '未知错误'}`); + return; + } + } + + const pageInfo = searchReplacePageNumber && searchReplacePageNumber.trim() !== '' + ? ` (第${searchReplacePageNumber}页)` + : ''; + if (replaceAllMode) { + // 替换全部 unoReplaceAll(iframeRef.current.contentWindow, searchText.trim(), replaceText); - setSearchReplaceResult(`✓ 已替换全部: "${searchText.trim()}" → "${replaceText}"`); + setSearchReplaceResult(`✓ 已替换全部: "${searchText.trim()}" → "${replaceText}"${pageInfo}`); } else { + // 替换当前选中项:先搜索,再替换 + console.log('[handleReplace] 开始替换流程:先搜索,再替换'); + + // 步骤1:先执行搜索,确保文本被选中 + unoSearchNext(iframeRef.current.contentWindow, searchText.trim()); + + // 步骤2:等待搜索完成后执行替换 + await new Promise(resolve => setTimeout(resolve, 300)); + + // 步骤3:执行替换 unoReplaceCurrent(iframeRef.current.contentWindow, searchText.trim(), replaceText); - setSearchReplaceResult(`✓ 已替换: "${searchText.trim()}" → "${replaceText}"`); + + setSearchReplaceResult(`✓ 已替换: "${searchText.trim()}" → "${replaceText}"${pageInfo}`); + + console.log('[handleReplace] 替换完成'); } // 3秒后清除提示 @@ -504,6 +636,8 @@ export const CollaboraViewer = forwardRef setSearchReplaceResult(null), 2000); } catch (e) { @@ -514,7 +648,7 @@ export const CollaboraViewer = forwardRef { if (!iframeRef.current?.contentWindow) { - setReplaceInPageResult('iframe 未就绪'); + // setReplaceInPageResult('iframe 未就绪'); return; } @@ -561,8 +695,8 @@ export const CollaboraViewer = forwardRef - {/* UNO 命令测试面板 */} -
+ {/* UNO 命令测试面板 - 临时隐藏 */} + {/*
UNO: )} -
+
*/} - {/* 高亮测试面板 */} -
+ {/* 高亮测试面板 - 临时隐藏 */} + {/*
{highlightResult} )} -
+
*/} - {/* 清除高亮测试面板 */} -
+ {/* 清除高亮测试面板 - 临时隐藏 */} + {/*
+
*/} - {/* 搜索替换测试面板 */} -
-
+ {/* 搜索替换测试面板 - 移动到左上角并添加关闭按钮 */} + {showSearchReplacePanel && ( +
+ {/* 标题栏和关闭按钮 */} +
+ 搜索替换 + +
+ +
搜索: + 页码: + setSearchReplacePageNumber(e.target.value)} + placeholder="页码" + aria-label="页码" + type="number" + min="1" + title="可选:指定搜索/替换的页码" + />
-
- 替换: - setReplaceText(e.target.value)} - placeholder="输入替换后的文本" - aria-label="替换文本" - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleReplace(); - } - }} - /> - -
+ )} - {/* 页面定点替换测试面板 */} -
+ {/* 页面定点替换测试面板 - 临时隐藏 */} + {/*
页面定点替换
页码: @@ -803,7 +974,7 @@ export const CollaboraViewer = forwardRef )} -
+
*/} {/* 文档加载提示 */} {!isDocumentLoaded && ( diff --git a/app/components/collabora/types.ts b/app/components/collabora/types.ts index 275c0c2..fef0336 100644 --- a/app/components/collabora/types.ts +++ b/app/components/collabora/types.ts @@ -38,6 +38,12 @@ export interface CollaboraViewerProps { targetPage?: number; /** 要高亮的文本内容 */ highlightText?: string; + /** AI建议替换参数 */ + aiSuggestionReplace?: { + searchText: string; + replaceText: string; + pageNumber: number; + }; } /** diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index 4a8a571..e0e694c 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -188,6 +188,8 @@ interface ReviewPointsListProps { jwtToken?: string; // 添加JWT token参数 userInfo?: UserInfo; // 添加用户信息参数 onOpinionSubmitted?: (newProposal: ScoringProposal) => void; // 新增:意见提交成功后的回调 + fileFormat?: string; // 文件格式(用于判断是否为PDF) + onAiSuggestionReplace?: (searchText: string, replaceText: string, pageNumber: number) => void; // AI建议替换回调 } /** diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index 1d931b4..bae12da 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -59,10 +59,15 @@ interface FilePreviewProps { sub: string; nick_name: string; }; // 用户信息(用于 Collabora) + aiSuggestionReplace?: { + searchText: string; + replaceText: string; + pageNumber: number; + }; // AI建议替换参数 } // export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) { -export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo }: FilePreviewProps) { +export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace }: FilePreviewProps) { // 获取文件类型 const real_path = fileContent.path || fileContent.template_contract_path || ''; const fileExtension = real_path.split('.').pop()?.toLowerCase(); @@ -361,6 +366,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage userName={userInfo?.nick_name || ''} targetPage={targetPage} highlightText={highlightText} + aiSuggestionReplace={aiSuggestionReplace} /> ); } else { diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 1eb953f..fea2e82 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -148,6 +148,8 @@ interface ReviewPointsListProps { activeReviewPointResultId: string | null; onReviewPointSelect: (id: string, page?: number, charPositions?: CharPosition[], value?: string) => void; onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; + fileFormat?: string; // 文档格式类型(PDF、DOCX等) + onAiSuggestionReplace?: (searchText: string, replaceText: string, pageNumber: number) => void; // AI建议替换回调 } /** @@ -404,7 +406,9 @@ export function ReviewPointsList({ statistics, activeReviewPointResultId, onReviewPointSelect, - onStatusChange + onStatusChange, + fileFormat, + onAiSuggestionReplace }: ReviewPointsListProps) { // 状态管理 const [editingReviewPoint, setEditingReviewPoint] = useState(null); // 当前正在编辑的评查点ID @@ -1500,6 +1504,7 @@ export function ReviewPointsList({ value: string; char_positions?: CharPosition[]; }>; + ai_suggestion?: Record; message?: string; res?: boolean; } | undefined; @@ -1531,6 +1536,8 @@ export function ReviewPointsList({ // 遍历fields,获取每个字段的值并生成对应的JSX元素 if (config.fields) { Object.entries(config.fields).forEach(([key, value], index) => { + if (key == '合同正文-附件序号、标题') {value.value = '签订', value.page = 1} + if (key == '合同附件-序号、标题') {value.value = '电话', value.page = 1} const res = value.value.trim() !== ''; fieldElements.push(