From 5f9ce2fe9f0921686ac8e971cf8767e3c020b73b Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Fri, 5 Dec 2025 21:38:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B5=B7=E8=8D=89=E5=90=88?= =?UTF-8?q?=E5=90=8C=E7=9A=84=E9=94=80=E6=AF=81=E4=BF=9D=E5=AD=98=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=92=8C=E5=88=A0=E9=99=A4=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/collabora/CollaboraViewer.tsx | 134 ++++++++++++++++-- .../collabora/lib/Highlightselecttext.ts | 10 +- app/components/collabora/types.ts | 3 + app/components/reviews/FilePreview.tsx | 3 +- app/routes/contract-draft.$draftId.tsx | 93 ++++++++---- app/services/collabora.wopi.server.ts | 4 +- 6 files changed, 195 insertions(+), 52 deletions(-) diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index 8e7ae14..45cb7ba 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -55,6 +55,8 @@ export const CollaboraViewer = forwardRef ({ unoCommands, isReady: isDocumentLoaded, @@ -150,6 +152,35 @@ export const CollaboraViewer = forwardRef { + const savedWindow = iframeWindowRef.current || iframeRef.current?.contentWindow; + if (savedWindow) { + console.log('[CollaboraViewer] 💾 父组件调用保存文档'); + try { + // 步骤1:发送保存命令 + sendUnoCommand(savedWindow, '.uno:Save', {}); + // console.log('[CollaboraViewer] ✓ 保存命令已发送'); + + // 步骤2:等待 WOPI PutFile 请求完成(增加到 2000ms) + // await new Promise(resolve => setTimeout(resolve, 2000)); + + // 步骤3:再次发送保存命令确保完全保存 + sendUnoCommand(savedWindow, '.uno:Save', {}); + // console.log('[CollaboraViewer] ✓ 二次保存命令已发送'); + + // 步骤4:再等待一段时间确保保存完成 + // await new Promise(resolve => setTimeout(resolve, 1000)); + + // console.log('[CollaboraViewer] ✓ 文档保存完成(总等待 3000ms)'); + } catch (error) { + console.error('[CollaboraViewer] ✗ 保存文档失败:', error); + throw error; + } + } else { + console.warn('[CollaboraViewer] ⚠️ 无法保存文档:iframe window 不可用'); + throw new Error('iframe window 不可用'); + } + }, }), [unoCommands, isDocumentLoaded, mode]); // 5. 监听 targetPage 和 highlightText 变化,自动跳转并高亮 @@ -182,7 +213,7 @@ export const CollaboraViewer = forwardRef { // 返回清理函数,在组件卸载时执行 return () => { const savedWindow = iframeWindowRef.current; if (savedWindow) { - console.log('[CollaboraViewer] 🔥 组件即将销毁,立即清除所有高亮'); + // console.log('[CollaboraViewer] 🔥 组件即将销毁,触发文档保存和清除高亮'); - // 立即触发清除操作,不等待异步完成 - // 使用 void 关键字表示我们不关心 Promise 的结果 + // 步骤1:发送保存命令(如果是编辑模式) + if (mode === 'edit') { + try { + console.log('[CollaboraViewer] 💾 组件销毁时发送保存命令'); + sendUnoCommand(savedWindow, '.uno:Save', {}); + // 再次发送确保保存 + setTimeout(() => { + sendUnoCommand(savedWindow, '.uno:Save', {}); + console.log('[CollaboraViewer] ✓ 二次保存命令已发送'); + }, 100); + } catch (error) { + console.error('[CollaboraViewer] ✗ 组件销毁时保存失败:', error); + } + } + + // 步骤2:清除高亮 void clearHighlights(savedWindow, { color: 16776960, // 黄色 timeout: 3000, @@ -217,7 +262,7 @@ export const CollaboraViewer = forwardRef { @@ -227,20 +272,32 @@ export const CollaboraViewer = forwardRef { + if (shouldAutoReplaceRef.current && searchText && replaceText && searchReplacePageNumber && isDocumentLoaded) { + console.log('[CollaboraViewer] 静默替换模式:自动执行替换:', { searchText, replaceText, searchReplacePageNumber }); + + // 重置标志 + shouldAutoReplaceRef.current = false; + + // 延迟执行,确保 DOM 更新完成 + const timer = setTimeout(async () => { + if (!iframeRef.current?.contentWindow) { + console.error('[CollaboraViewer] iframe 未就绪,无法执行替换'); + return; + } + + try { + const pageNumber = parseInt(searchReplacePageNumber, 10); + + // 步骤1:跳转到指定页 + console.log(`[CollaboraViewer] 步骤1:跳转到第 ${pageNumber} 页`); + await customGotoPage(iframeRef.current.contentWindow, pageNumber); + + // 等待页面渲染 + await new Promise(resolve => setTimeout(resolve, 300)); + + // 步骤2:搜索文本(确保文本被选中) + console.log(`[CollaboraViewer] 步骤2:搜索文本 "${searchText}"`); + unoSearchNext(iframeRef.current.contentWindow, searchText); + + // 等待搜索完成 + await new Promise(resolve => setTimeout(resolve, 300)); + + // 步骤3:执行替换 + console.log(`[CollaboraViewer] 步骤3:替换为 "${replaceText}"`); + unoReplaceCurrent(iframeRef.current.contentWindow, searchText, replaceText); + + console.log('[CollaboraViewer] ✓ 静默替换完成'); + + // 显示成功提示(可选) + // toastService.success(`已替换: "${searchText}" → "${replaceText}"`); + } catch (error) { + console.error('[CollaboraViewer] 静默替换失败:', error); + } + }, 300); + + return () => clearTimeout(timer); + } + }, [searchText, replaceText, searchReplacePageNumber, isDocumentLoaded]); // eslint-disable-line react-hooks/exhaustive-deps + // 加载中状态 if (loading) { return ( diff --git a/app/components/collabora/lib/Highlightselecttext.ts b/app/components/collabora/lib/Highlightselecttext.ts index 96b0ff9..e044e19 100644 --- a/app/components/collabora/lib/Highlightselecttext.ts +++ b/app/components/collabora/lib/Highlightselecttext.ts @@ -73,11 +73,11 @@ export async function highlightText( // const page = options?.page ?? 1; // 默认第1页 const page = options?.page ?? null; // 默认第1页 - console.log('[HighlightSelectText] 调用 Python 脚本高亮文本:', { - text, - color, - page - }); + // console.log('[HighlightSelectText] 调用 Python 脚本高亮文本:', { + // text, + // color, + // page + // }); try { // 调用 Python 脚本: HighlightAndJumpToPage diff --git a/app/components/collabora/types.ts b/app/components/collabora/types.ts index 550eafe..8e35930 100644 --- a/app/components/collabora/types.ts +++ b/app/components/collabora/types.ts @@ -43,6 +43,7 @@ export interface CollaboraViewerProps { searchText: string; replaceText: string; pageNumber: number; + silentReplace?: boolean; // 是否静默替换(不显示面板) }; } @@ -65,4 +66,6 @@ export interface CollaboraViewerHandle { getIframeWindow: () => Window | null; /** 清除所有高亮 */ clearAllHighlights: () => Promise; + /** 保存文档 */ + saveDocument: () => Promise; } diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index d90af37..456d7a6 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -63,6 +63,7 @@ interface FilePreviewProps { searchText: string; replaceText: string; pageNumber: number; + silentReplace?: boolean; // 是否静默替换(不显示面板) }; // AI建议替换参数 isTemplate?: boolean; } @@ -148,7 +149,7 @@ export const FilePreview = forwardRef(funct if (intervalCleared) return; if (!collaboraViewerRef.current?.isReady) { - console.log('[FilePreview] 等待 Collabora 就绪...'); + // console.log('[FilePreview] 等待 Collabora 就绪...'); return; } diff --git a/app/routes/contract-draft.$draftId.tsx b/app/routes/contract-draft.$draftId.tsx index 78aa5b5..9b953ba 100644 --- a/app/routes/contract-draft.$draftId.tsx +++ b/app/routes/contract-draft.$draftId.tsx @@ -184,14 +184,17 @@ export default function ContractDraftPage() { searchText: string; replaceText: string; pageNumber: number; + silentReplace?: boolean; } | undefined>(undefined); + const [showFilePreview, setShowFilePreview] = useState(true); // 控制 FilePreview 显示 + const [isCompleting, setIsCompleting] = useState(false); // 标记是否正在执行完成流程 const filePreviewRef = useRef(null); const hasDeletedNormally = useRef(false); // 标记是否已通过正常流程删除 const currentPathRef = useRef(window.location.pathname); // 保存当前路由路径 // 从 fetcher.state 判断是否正在操作 - const isDeleting = fetcher.state !== 'idle'; + const isDeleting = fetcher.state !== 'idle' || isCompleting; // 处理 fetcher 响应(文件删除) useEffect(() => { @@ -275,24 +278,25 @@ export default function ContractDraftPage() { const placeholder = `{{${key}}}`; console.log(`[Draft] 单个替换: ${placeholder} -> ${value}`); - // 设置 AI 建议替换参数,触发 FilePreview 中的替换 + // 设置 AI 建议替换参数,触发 FilePreview 中的静默替换 setAiSuggestionReplace({ searchText: placeholder, replaceText: value, - pageNumber: 1 // 从第一页开始搜索 + pageNumber: 1, // 从第一页开始搜索 + silentReplace: true // 静默替换,不显示搜索替换面板 }); // 短暂延迟后清除参数,以便下次可以重新触发 setTimeout(() => { setAiSuggestionReplace(undefined); // toastService.success(`已替换 ${key}`); - }, 1000); + }, 2000); }; // 字段聚焦时高亮对应占位符 const handleFieldFocus = (key: string) => { const placeholder = `{{${key}}}`; - console.log(`[Draft] 高亮占位符: ${placeholder}`); + // console.log(`[Draft] 高亮占位符: ${placeholder}`); // 设置高亮值,触发 FilePreview 中的高亮 setHighlightValue(placeholder); @@ -303,7 +307,7 @@ export default function ContractDraftPage() { }, 100); }; - // 导出文档(下载当前编辑的文件) + // 导出文档(下载当前编辑的文件)- 不再需要手动保存 const handleExportDocument = async () => { if (!draft.file_path) { toastService.error('文件路径不存在,无法下载'); @@ -311,6 +315,9 @@ export default function ContractDraftPage() { } try { + const fileExtension = draft.file_path.split('.').pop()?.toLowerCase(); + + console.log('[Draft] 正在下载文件:', draft.file_path); toastService.info('正在下载文件...'); // 使用 axios-client 的 downloadFile 方法下载文件 @@ -320,8 +327,7 @@ export default function ContractDraftPage() { const blobUrl = URL.createObjectURL(blob); // 清理文件名 - const fileExtension = draft.file_path.split('.').pop() || 'docx'; - const fileName = `${draft.title}.${fileExtension}`; + const fileName = `${draft.title}.${fileExtension || 'docx'}`; const cleanFileName = fileName.replace(/[<>:"/\\|?*]/g, '_'); // 创建隐藏的a标签并点击下载 @@ -342,24 +348,41 @@ export default function ContractDraftPage() { } catch (error) { console.error('[Draft] 下载文件失败:', error); toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); + throw error; // 重新抛出错误,让 handleComplete 捕获 } }; - // 完成起草(下载文件 + 删除 MinIO 文件) + // 完成起草(触发保存 → 下载文件 → 删除 MinIO 文件) const handleComplete = async () => { try { - // 1. 先下载文件 + setIsCompleting(true); + + // 步骤1:隐藏 FilePreview,触发 CollaboraViewer 组件销毁和保存 + console.log('[Complete] 步骤1:隐藏 FilePreview,触发 Collabora 保存'); + toastService.info('正在保存文档...'); + setShowFilePreview(false); + + // 步骤2:等待 Collabora 保存完成(给予充足时间让 WOPI PutFile 完成) + console.log('[Complete] 步骤2:等待 4 秒让 WOPI PutFile 完成'); + await new Promise(resolve => setTimeout(resolve, 4000)); + + // 步骤3:下载文件 + console.log('[Complete] 步骤3:下载文件'); await handleExportDocument(); - // 2. 删除 MinIO 文件 + // 步骤4:删除 MinIO 文件 + console.log('[Complete] 步骤4:删除 MinIO 文件'); const formData = new FormData(); formData.append('_action', 'deleteFile'); formData.append('filePath', draft.file_path); fetcher.submit(formData, { method: 'post' }); + } catch (error) { console.error('[Complete] 操作失败:', error); toastService.error('操作失败,请重试'); + setIsCompleting(false); + setShowFilePreview(true); // 恢复显示 } }; @@ -417,25 +440,35 @@ export default function ContractDraftPage() {
{/* 左侧:文档预览(60%) */}
- + {showFilePreview ? ( + + ) : ( +
+
+
+

正在保存文档...

+

请稍候,即将完成

+
+
+ )}
{/* 右侧:占位符表单(40%) */} diff --git a/app/services/collabora.wopi.server.ts b/app/services/collabora.wopi.server.ts index 927e1f3..86ccd14 100644 --- a/app/services/collabora.wopi.server.ts +++ b/app/services/collabora.wopi.server.ts @@ -170,7 +170,7 @@ export class WopiService { // 功能配置 DisableInactiveMessages: true, - DisableAutoSave: true, + DisableAutoSave: false, // 文件最后修改时间 LastModifiedTime: lastModified || new Date().toISOString(), @@ -260,7 +260,7 @@ export class WopiService { throw new Error(`保存文件失败: ${sanitizedFileId}`); } - // console.log(`PutFile 成功: ${sanitizedFileId}, Size: ${fileBuffer.byteLength} bytes`); + console.log(`PutFile 成功: ${sanitizedFileId}, Size: ${fileBuffer.byteLength} bytes`); } catch (error) { console.error('PutFile 失败:', error); throw error;