/** * Collabora Online 文档查看器组件 * * 功能: * - 加载 Collabora Online iframe * - 管理文档加载状态 * - 提供 UNO 命令接口 * - 支持只读和编辑模式 * * @encoding UTF-8 */ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { useCollaboraConfig, useCollaboraUnoCommands, useDocumentReady } from './hooks'; import { customGotoPage, highlightText as pythonHighlightText, replaceTextInPage, requestPageInfo, unoCancelSearch, unoHighlightText, unoReplaceAll, unoReplaceCurrent, unoScrollToTop, unoSearchNext, type GotoPageResponse, type PageInfo } from './lib'; import { clearHighlights } from './lib/ClearHighlight'; import type { CollaboraViewerHandle, CollaboraViewerProps } from './types'; import { sendUnoCommand } from './Uno'; /** * Collabora 文档查看器组件 * @param props - 组件属性 * @param ref - 父组件传入的 ref,用于暴露命令接口 */ export const CollaboraViewer = forwardRef( function CollaboraViewer( { fileId, mode = 'view', userId = 'guest', userName = '', targetPage, highlightText, aiSuggestionReplace, }, ref ) { const iframeRef = useRef(null); // 保存 iframe 的 contentWindow 引用,用于组件卸载时清除高亮 const iframeWindowRef = useRef(null); // 搜索替换面板显示状态 const [showSearchReplacePanel, setShowSearchReplacePanel] = useState(false); // 标记是否应该自动执行搜索 const shouldAutoSearchRef = useRef(false); // 标记是否应该自动执行替换(静默模式) const shouldAutoReplaceRef = useRef(false); // 高亮测试面板状态 const [highlightTextInput, setHighlightTextInput] = useState(''); const [highlightPage, setHighlightPage] = useState(''); const [previousHighlightText, setPreviousHighlightText] = useState(null); const [highlightResult, setHighlightResult] = useState(null); // 清除高亮测试面板状态 const [clearHighlightResult, setClearHighlightResult] = useState(null); const [isClearing, setIsClearing] = useState(false); // 页数信息测试状态 const [pageInfoResult, setPageInfoResult] = useState(null); const [isLoadingPageInfo, setIsLoadingPageInfo] = useState(false); // 页面跳转测试状态 const [gotoPageInput, setGotoPageInput] = useState(''); const [gotoPageResult, setGotoPageResult] = useState(null); const [isJumping, setIsJumping] = useState(false); // UNO 命令测试面板状态 const [unoCmd, setUnoCmd] = useState('.uno:Bold'); const [unoArgs, setUnoArgs] = useState(''); const [unoResult, setUnoResult] = useState(null); // 搜索替换测试面板状态 const [searchText, setSearchText] = useState(''); const [replaceText, setReplaceText] = useState(''); const [searchReplaceResult, setSearchReplaceResult] = useState(null); const [replaceAllMode, setReplaceAllMode] = useState(false); const [searchReplacePageNumber, setSearchReplacePageNumber] = useState(''); // 记录上一次搜索的参数,用于判断是否需要重新跳转页面 const [lastSearchParams, setLastSearchParams] = useState<{ text: string; page: string; }>({ text: '', page: '' }); // 页面定点替换状态 const [replaceInPageNumber, setReplaceInPageNumber] = useState(''); const [replaceInPageSearch, setReplaceInPageSearch] = useState(''); const [replaceInPageReplace, setReplaceInPageReplace] = useState(''); const [replaceInPageResult, setReplaceInPageResult] = useState(null); const [isReplacingInPage, setIsReplacingInPage] = useState(false); // 1. 加载 Collabora 配置 const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName); // 2. 监听文档加载状态 const { isDocumentLoaded } = useDocumentReady(iframeRef); // 2.5. 保存 iframe window 引用并在文档加载时清除所有高亮 useEffect(() => { if (isDocumentLoaded && iframeRef.current?.contentWindow) { iframeWindowRef.current = iframeRef.current.contentWindow; // console.log('[CollaboraViewer] 已保存 iframe window 引用'); // 🔥 文档加载完成后主动清除一次高亮(防止缓存的高亮状态) // console.log('[CollaboraViewer] 🧹 文档加载完成,清除可能存在的缓存高亮'); clearHighlights(iframeRef.current.contentWindow, { color: 16776960, timeout: 5000, }).then((result) => { if (result.count && result.count > 0) { // console.log(`[CollaboraViewer] ✓ 清除了 ${result.count} 个缓存的高亮区域`); } else { // console.log('[CollaboraViewer] ✓ 文档无缓存高亮,已确认干净'); } }).catch(error => { console.warn('[CollaboraViewer] ⚠️ 清除缓存高亮失败:', error); }); } }, [isDocumentLoaded]); // 3. UNO 命令封装 const unoCommands = useCollaboraUnoCommands(iframeRef); // 4. 暴露接口给父组件(包括清除高亮方法和保存方法) useImperativeHandle(ref, () => ({ unoCommands, isReady: isDocumentLoaded, mode, getIframeWindow: () => iframeRef.current?.contentWindow || null, clearAllHighlights: async () => { const savedWindow = iframeWindowRef.current || iframeRef.current?.contentWindow; if (savedWindow) { // console.log('[CollaboraViewer] 🧹 父组件调用清除高亮'); await clearHighlights(savedWindow, { color: 16776960, timeout: 5000, }); // console.log('[CollaboraViewer] ✓ 清除高亮完成'); } else { console.warn('[CollaboraViewer] ⚠️ 无法清除高亮:iframe window 不可用'); } }, saveDocument: async () => { 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 变化,自动跳转并高亮 // 新逻辑:从指定页(默认第1页)开始向下搜索第一个匹配项并高亮 // - 先跳转到指定页面 // - 使用 UNO 搜索从当前位置向下找第一个匹配项(UNO 搜索自带循环功能) // - 对搜索到的文本设置高亮背景色 useEffect(() => { // 如果文档未加载完成,不执行跳转和高亮 if (!isDocumentLoaded || !iframeRef.current?.contentWindow) { return; } // 如果有高亮文本,执行高亮操作 if (highlightText && highlightText.trim() !== '') { const performHighlight = async () => { const iframeWindow = iframeRef.current!.contentWindow!; const textToHighlight = highlightText.trim(); // 确定起始页码:有 targetPage 则使用,否则默认从第1页开始 const startPage = (targetPage !== undefined && targetPage !== null) ? targetPage : 1; try { // 步骤1:清除之前的所有高亮 // console.log('[CollaboraViewer] 步骤1:清除旧高亮...'); await clearHighlights(iframeWindow, { color: 16776960, // 黄色 timeout: 5000, }); // 短暂延迟,确保清除操作完成 await new Promise(resolve => setTimeout(resolve, 100)); // 步骤2:跳转到起始页面 // console.log(`[CollaboraViewer] 步骤2:跳转到第 ${startPage} 页...`); try { await customGotoPage(iframeWindow, startPage); // 等待页面跳转完成 await new Promise(resolve => setTimeout(resolve, 300)); } catch (gotoError) { console.warn('[CollaboraViewer] 页面跳转失败,继续在当前位置搜索:', gotoError); } // 步骤3:使用 UNO 搜索从当前位置向下找第一个匹配项 // UNO 搜索会自动跳转到匹配位置,如果到文档末尾没找到会循环回开头继续搜索 console.log(`[CollaboraViewer] 步骤3:从第 ${startPage} 页开始向下搜索...`); unoSearchNext(iframeWindow, textToHighlight); // 等待搜索完成 await new Promise(resolve => setTimeout(resolve, 200)); // 步骤4:对搜索到的文本(当前选中)设置高亮背景色 // console.log('[CollaboraViewer] 步骤4:设置高亮背景色...'); sendUnoCommand(iframeWindow, '.uno:BackColor', { BackColor: { type: 'long', value: 16776960 }, // 黄色 }); // 短暂延迟,确保高亮操作完成 await new Promise(resolve => setTimeout(resolve, 100)); // 步骤5:取消选中状态(避免高亮后文本仍被选中) // console.log('[CollaboraViewer] 步骤5:取消选中状态...'); sendUnoCommand(iframeWindow, '.uno:Escape', {}); console.log(`[CollaboraViewer] ✓ 搜索高亮完成:(从第${startPage}页开始搜索)`); } catch (error) { console.error('[CollaboraViewer] 高亮失败:', error); } }; performHighlight(); } }, [targetPage, highlightText, isDocumentLoaded]); // 6. 组件销毁时保存文档并清除所有高亮(使用保存的 window 引用) useEffect(() => { // 返回清理函数,在组件卸载时执行 return () => { const savedWindow = iframeWindowRef.current; if (savedWindow) { // console.log('[CollaboraViewer] 🔥 组件即将销毁,触发文档保存和清除高亮'); // 步骤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, }).then(() => { console.log('[CollaboraViewer] ✓ 组件销毁时高亮清除成功'); }).catch(error => { console.error('[CollaboraViewer] ✗ 组件销毁时清除高亮失败:', error); }); // 清空引用 iframeWindowRef.current = null; } else { console.warn('[CollaboraViewer] ⚠️ 组件销毁时未找到保存的 window 引用'); } }; }, [mode]); // 7. 监听 AI 建议替换参数变化,设置搜索参数 useEffect(() => { if (!aiSuggestionReplace || !isDocumentLoaded) { return; } console.log('[CollaboraViewer] 收到 AI 建议替换参数:', aiSuggestionReplace); const { searchText: newSearchText, replaceText: newReplaceText, pageNumber, silentReplace } = aiSuggestionReplace; // 根据 silentReplace 标志决定是否显示面板 if (silentReplace) { // 静默替换模式:不显示面板,直接执行替换 console.log('[CollaboraViewer] 静默替换模式,不显示面板,直接执行替换'); // 延迟执行替换,确保 DOM 更新完成 const timer = setTimeout(async () => { if (!iframeRef.current?.contentWindow) { console.error('[CollaboraViewer] iframe 未就绪,无法执行替换'); return; } try { // 步骤1:搜索文本(确保文本被选中,会自动跳转到匹配位置) console.log(`[CollaboraViewer] 步骤1:搜索文本 "${newSearchText}"`); unoSearchNext(iframeRef.current.contentWindow, newSearchText); // 等待搜索完成 await new Promise(resolve => setTimeout(resolve, 300)); // 步骤2:执行替换(替换后光标保留在当前位置) console.log(`[CollaboraViewer] 步骤2:替换为 "${newReplaceText}"`); unoReplaceCurrent(iframeRef.current.contentWindow, newSearchText, newReplaceText); // 等待替换完成 await new Promise(resolve => setTimeout(resolve, 200)); // 步骤3:自动搜索下一个相同的占位符(如果还有的话) // console.log(`[CollaboraViewer] 步骤3:定位到下一个 "${newSearchText}"`); unoSearchNext(iframeRef.current.contentWindow, newSearchText); console.log('[CollaboraViewer] ✓ 静默替换完成,已定位到下一个占位符(如有)'); } catch (error) { console.error('[CollaboraViewer] 静默替换失败:', error); } }, 300); return () => clearTimeout(timer); } else { // 显示搜索替换面板 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 // 9. 当搜索参数更新完成后,自动执行替换(静默模式) // 不跳转页面,直接在当前位置搜索并替换,替换后自动定位到下一个相同占位符 useEffect(() => { if (shouldAutoReplaceRef.current && searchText && replaceText && isDocumentLoaded) { console.log('[CollaboraViewer] 静默替换模式:自动执行替换:', { searchText, replaceText }); // 重置标志 shouldAutoReplaceRef.current = false; // 延迟执行,确保 DOM 更新完成 const timer = setTimeout(async () => { if (!iframeRef.current?.contentWindow) { console.error('[CollaboraViewer] iframe 未就绪,无法执行替换'); return; } try { // 步骤1:搜索文本(确保文本被选中,会自动跳转到匹配位置) console.log(`[CollaboraViewer] 步骤1:搜索文本 "${searchText}"`); unoSearchNext(iframeRef.current.contentWindow, searchText); // 等待搜索完成 await new Promise(resolve => setTimeout(resolve, 300)); // 步骤2:执行替换(替换后光标保留在当前位置) console.log(`[CollaboraViewer] 步骤2:替换为 "${replaceText}"`); unoReplaceCurrent(iframeRef.current.contentWindow, searchText, replaceText); // 等待替换完成 await new Promise(resolve => setTimeout(resolve, 200)); // 步骤3:自动搜索下一个相同的占位符(如果还有的话) console.log(`[CollaboraViewer] 步骤3:定位到下一个 "${searchText}"`); unoSearchNext(iframeRef.current.contentWindow, searchText); console.log('[CollaboraViewer] ✓ 静默替换完成,已定位到下一个占位符(如有)'); } catch (error) { console.error('[CollaboraViewer] 静默替换失败:', error); } }, 300); return () => clearTimeout(timer); } }, [searchText, replaceText, isDocumentLoaded]); // eslint-disable-line react-hooks/exhaustive-deps // 加载中状态 if (loading) { return (

加载文档配置中...

); } // 错误状态 if (error || !config) { return (

{error}

{/*

请刷新页面重试或联系管理员

*/}
); } // 发送 UNO 命令的处理函数(测试用) const sendUno = () => { if (!iframeRef.current?.contentWindow) { setUnoResult('iframe 不可用'); return; } let args: Record = {}; const raw = (unoArgs || '').trim(); if (raw !== '') { try { args = JSON.parse(raw) as Record; } catch (err) { try { // fallback: replace single quotes with double quotes and parse args = JSON.parse(raw.replace(/'(.*?)'/g, '"$1"')) as Record; } catch (err2) { console.error('解析 UNO Args 失败:', err2); setUnoResult('Args 解析失败,请使用有效 JSON'); return; } } } try { // 使用正确的 sendUnoCommand 方法 sendUnoCommand(iframeRef.current.contentWindow, unoCmd, args); setUnoResult(`已发送: ${unoCmd}`); console.log('[UNO Debug] 发送命令:', unoCmd, args); } catch (e) { console.error('发送 UNO 失败:', e); setUnoResult('发送失败,请查看控制台'); } }; // 高亮测试处理函数 const handleSwitchHighlight = async () => { if (!iframeRef.current?.contentWindow) { // setHighlightResult('iframe 未就绪'); return; } if (!highlightTextInput || highlightTextInput.trim() === '') { setHighlightResult('请输入要高亮的文本'); return; } try { // 解析页码 (可选) const page = highlightPage && highlightPage.trim() !== '' ? parseInt(highlightPage.trim(), 10) : undefined; // 验证页码 if (page !== undefined && (isNaN(page) || page < 1)) { setHighlightResult('页码必须是大于0的整数'); return; } const iframeWindow = iframeRef.current.contentWindow; const textToHighlight = highlightTextInput.trim(); // 先清除旧高亮 await clearHighlights(iframeWindow, { color: 16776960, timeout: 5000, }); await new Promise(resolve => setTimeout(resolve, 100)); // 根据是否有页码选择高亮方式 if (page !== undefined) { // 使用 Python 脚本(跨页搜索 + 高亮 + 跳转) const result = await pythonHighlightText(iframeWindow, textToHighlight, { color: 16776960, page: page, }); if (result.success) { setPreviousHighlightText(textToHighlight); setHighlightResult(`✓ 已切换高亮: ${textToHighlight} (第${page}页, 共${result.highlightedCount}处)`); } else { setHighlightResult(`✗ 高亮失败: ${result.message}`); } } else { // 使用 UNO 命令(当前页面高亮) unoHighlightText(iframeWindow, textToHighlight, 16776960); await new Promise(resolve => setTimeout(resolve, 100)); sendUnoCommand(iframeWindow, '.uno:Escape', {}); setPreviousHighlightText(textToHighlight); setHighlightResult(`✓ 已切换高亮: ${textToHighlight} (当前页)`); } } catch (e) { console.error('切换高亮失败:', e); setHighlightResult(`切换失败: ${e instanceof Error ? e.message : '未知错误'}`); } }; // 滚动到顶部处理函数 const handleScrollToTop = async () => { if (!iframeRef.current?.contentWindow) { console.error('iframe 未就绪'); return; } try { await unoScrollToTop(iframeRef.current.contentWindow); // console.log('[CollaboraViewer] 已滚动到文档顶部'); } catch (e) { console.error('滚动到顶部失败:', e); } }; // 获取页数信息处理函数 const handleGetPageInfo = async () => { if (!iframeRef.current?.contentWindow) { // setPageInfoResult('iframe 未就绪'); return; } setIsLoadingPageInfo(true); setPageInfoResult('正在获取页数信息...'); try { const info: PageInfo = await requestPageInfo(iframeRef.current.contentWindow); setPageInfoResult( `✓ 总页数: ${info.totalPages} | 当前页: ${info.currentPage + 1} (索引: ${info.currentPage})` ); console.log('[CollaboraViewer] 页数信息:', info); // 3秒后清除提示 setTimeout(() => { setPageInfoResult(null); }, 3000); } catch (e) { console.error('获取页数信息失败:', e); setPageInfoResult(`✗ 获取失败: ${e instanceof Error ? e.message : '未知错误'}`); // 5秒后清除错误提示 setTimeout(() => { setPageInfoResult(null); }, 5000); } finally { setIsLoadingPageInfo(false); } }; // 页面跳转处理函数 const handleGotoPage = async () => { if (!iframeRef.current?.contentWindow) { // setGotoPageResult('iframe 未就绪'); return; } const pageNumber = parseInt(gotoPageInput.trim(), 10); if (isNaN(pageNumber) || pageNumber < 1) { setGotoPageResult('请输入有效的页码 (大于0的整数)'); return; } setIsJumping(true); // setGotoPageResult(`正在跳转到第 ${pageNumber} 页...`); try { const response: GotoPageResponse = await customGotoPage( iframeRef.current.contentWindow, pageNumber ); setGotoPageResult( `✓ 已跳转到第 ${response.pageNumber} 页 (总页数: ${response.totalPages})` ); // console.log('[CollaboraViewer] 页面跳转成功:', response); // 3秒后清除提示 setTimeout(() => { setGotoPageResult(null); }, 3000); } catch (e) { console.error('页面跳转失败:', e); setGotoPageResult(`✗ 跳转失败: ${e instanceof Error ? e.message : '未知错误'}`); // 5秒后清除错误提示 setTimeout(() => { setGotoPageResult(null); }, 5000); } finally { setIsJumping(false); } }; // 清除高亮处理函数 const handleClearHighlights = async () => { if (!iframeRef.current?.contentWindow) { // setClearHighlightResult('iframe 未就绪'); return; } setIsClearing(true); setClearHighlightResult('正在清除高亮...'); try { const result = await clearHighlights(iframeRef.current.contentWindow, { color: 16776960, // 黄色 timeout: 10000, }); if (result.success) { setClearHighlightResult(`✓ ${result.message}`); // console.log('[CollaboraViewer] 清除高亮成功:', result); // 清空之前记录的高亮文本 setPreviousHighlightText(null); } else { setClearHighlightResult(`✗ 清除失败: ${result.message}`); console.error('[CollaboraViewer] 清除高亮失败:', result); } // 3秒后清除提示 setTimeout(() => { setClearHighlightResult(null); }, 3000); } catch (e) { console.error('清除高亮失败:', e); setClearHighlightResult(`✗ 清除失败: ${e instanceof Error ? e.message : '未知错误'}`); // 5秒后清除错误提示 setTimeout(() => { setClearHighlightResult(null); }, 5000); } finally { setIsClearing(false); } }; // 搜索下一个处理函数 const handleSearchNext = async () => { if (!iframeRef.current?.contentWindow) { // setSearchReplaceResult('iframe 未就绪'); return; } if (!searchText.trim()) { setSearchReplaceResult('请输入搜索文本'); return; } try { const currentSearchText = searchText.trim(); const currentPageNumber = searchReplacePageNumber.trim(); // 判断是否需要跳转页面(搜索文本或页码发生变化) const needsPageJump = lastSearchParams.text !== currentSearchText || lastSearchParams.page !== currentPageNumber; // 如果指定了页码且需要跳转,先跳转到该页 if (needsPageJump && currentPageNumber !== '') { const pageNumber = parseInt(currentPageNumber, 10); if (isNaN(pageNumber) || pageNumber < 1) { setSearchReplaceResult('✗ 页码必须是大于0的整数'); return; } // setSearchReplaceResult(`正在跳转到第 ${pageNumber} 页...`); try { await customGotoPage(iframeRef.current.contentWindow, pageNumber); // 短暂延迟等待页面渲染 await new Promise(resolve => 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); } catch (e) { console.error('搜索失败:', e); setSearchReplaceResult(`✗ 搜索失败: ${e instanceof Error ? e.message : '未知错误'}`); } }; // 替换处理函数 const handleReplace = async () => { if (!iframeRef.current?.contentWindow) { // setSearchReplaceResult('iframe 未就绪'); return; } if (!searchText.trim()) { setSearchReplaceResult('请输入搜索文本'); return; } try { // 如果指定了页码,先跳转到该页 if (searchReplacePageNumber && searchReplacePageNumber.trim() !== '') { const pageNumber = parseInt(searchReplacePageNumber.trim(), 10); if (isNaN(pageNumber) || pageNumber < 1) { setSearchReplaceResult('✗ 页码必须是大于0的整数'); return; } // setSearchReplaceResult(`正在跳转到第 ${pageNumber} 页...`); try { await customGotoPage(iframeRef.current.contentWindow, pageNumber); // 短暂延迟等待页面渲染 await new Promise(resolve => 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}"${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}"${pageInfo}`); console.log('[handleReplace] 替换完成'); } // 3秒后清除提示 setTimeout(() => setSearchReplaceResult(null), 3000); } catch (e) { console.error('替换失败:', e); setSearchReplaceResult(`✗ 替换失败: ${e instanceof Error ? e.message : '未知错误'}`); } }; // 取消搜索处理函数 const handleCancelSearch = () => { if (!iframeRef.current?.contentWindow) { return; } try { unoCancelSearch(iframeRef.current.contentWindow); // 重置搜索参数记录,下次搜索会重新定位 setLastSearchParams({ text: '', page: '' }); setSearchReplaceResult('✓ 已取消搜索'); setTimeout(() => setSearchReplaceResult(null), 2000); } catch (e) { console.error('取消搜索失败:', e); } }; // 页面定点替换处理函数 const handleReplaceInPage = async () => { if (!iframeRef.current?.contentWindow) { // setReplaceInPageResult('iframe 未就绪'); return; } const pageNum = parseInt(replaceInPageNumber.trim(), 10); if (isNaN(pageNum) || pageNum < 1) { setReplaceInPageResult('请输入有效的页码 (大于0的整数)'); return; } if (!replaceInPageSearch.trim()) { setReplaceInPageResult('请输入原文'); return; } setIsReplacingInPage(true); setReplaceInPageResult(`正在第 ${pageNum} 页执行替换...`); try { const result = await replaceTextInPage( iframeRef.current.contentWindow, pageNum, replaceInPageSearch.trim(), replaceInPageReplace ); if (result.success) { setReplaceInPageResult(`✓ ${result.message}`); } else { setReplaceInPageResult(`✗ ${result.message}`); } // 5秒后清除提示 setTimeout(() => setReplaceInPageResult(null), 5000); } catch (e) { console.error('页面定点替换失败:', e); setReplaceInPageResult(`✗ 执行失败: ${e instanceof Error ? e.message : '未知错误'}`); // 5秒后清除错误提示 setTimeout(() => setReplaceInPageResult(null), 5000); } finally { setIsReplacingInPage(false); } }; return (
{/* UNO 命令测试面板 - 临时隐藏 */} {/*
UNO: setUnoCmd(e.target.value)} placeholder="UNO 命令 (如 .uno:Bold)" aria-label="UNO 命令" onKeyPress={(e) => { if (e.key === 'Enter') { sendUno(); } }} /> setUnoArgs(e.target.value)} placeholder='Args (JSON, 如 {"SearchItem.SearchString":{"type":"string","value":"test"}})' aria-label="UNO Args (JSON)" onKeyPress={(e) => { if (e.key === 'Enter') { sendUno(); } }} /> {unoResult && ( {unoResult} )}
*/} {/* 高亮测试面板 - 临时隐藏 */} {/*
setHighlightTextInput(e.target.value)} placeholder="输入要高亮的文本 (如: 合同编号)" aria-label="高亮文本" onKeyPress={(e) => { if (e.key === 'Enter') { handleSwitchHighlight(); } }} /> setHighlightPage(e.target.value)} placeholder="页码" aria-label="页码" type="number" min="1" onKeyPress={(e) => { if (e.key === 'Enter') { handleSwitchHighlight(); } }} /> {previousHighlightText && ( 上次: {previousHighlightText} )} {highlightResult && ( {highlightResult} )}
*/} {/* 清除高亮测试面板 - 临时隐藏 */} {/*
{clearHighlightResult && ( {clearHighlightResult} )}
*/} {/* 搜索替换测试面板 - 移动到左上角并添加关闭按钮 */} {showSearchReplacePanel && (
{/* 标题栏和关闭按钮 */}
搜索替换
搜索: setSearchText(e.target.value)} placeholder="输入要搜索的文本" aria-label="搜索文本" onKeyDown={(e) => { if (e.key === 'Enter') { handleSearchNext(); } }} /> 页码: 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(); } }} /> {/* */}
{searchReplaceResult && (
{searchReplaceResult}
)}
)} {/* 页面定点替换测试面板 - 临时隐藏 */} {/*
页面定点替换
页码: setReplaceInPageNumber(e.target.value)} placeholder="如: 12" aria-label="页码" type="number" min="1" /> 原文: setReplaceInPageSearch(e.target.value)} placeholder="要替换的文本" aria-label="原文" />
新文本: setReplaceInPageReplace(e.target.value)} placeholder="替换后的文本" aria-label="新文本" onKeyDown={(e) => { if (e.key === 'Enter') { handleReplaceInPage(); } }} />
{replaceInPageResult && (
{replaceInPageResult}
)}
*/} {/* 文档加载提示 */} {!isDocumentLoaded && (

正在加载文档...

{config.fileName}

)} {/* Collabora iframe - tabIndex is needed for keyboard navigation */} {/* eslint-disable jsx-a11y/no-noninteractive-tabindex */}