/** * Collabora Online 文档查看器组件 * * 功能: * - 加载 Collabora Online iframe * - 管理文档加载状态 * - 提供 UNO 命令接口 * - 支持只读和编辑模式 * * @encoding UTF-8 */ import { useRef, forwardRef, useImperativeHandle, useState } from 'react'; import type { CollaboraViewerProps, CollaboraViewerHandle } from './types'; import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from './hooks'; import { sendUnoCommand } from './Uno'; import { highlightText } from './lib/Highlightselecttext'; import { clearHighlights } from './lib/ClearHighlight'; import { unoScrollToTop, requestPageInfo, customGotoPage, unoSearchNext, unoReplaceCurrent, unoReplaceAll, unoCancelSearch, replaceTextInPage, type PageInfo, type GotoPageResponse } from './lib'; /** * Collabora 文档查看器组件 * @param props - 组件属性 * @param ref - 父组件传入的 ref,用于暴露命令接口 */ export const CollaboraViewer = forwardRef( function CollaboraViewer( { fileId, mode = 'view', userId = 'guest', userName = '', }, ref ) { const iframeRef = useRef(null); // 高亮测试面板状态 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 [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); // 3. UNO 命令封装 const unoCommands = useCollaboraUnoCommands(iframeRef); // 4. 暴露接口给父组件 useImperativeHandle(ref, () => ({ unoCommands, isReady: isDocumentLoaded, mode, getIframeWindow: () => iframeRef.current?.contentWindow || null, }), [unoCommands, isDocumentLoaded, mode]); // 加载中状态 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; } await highlightText( iframeRef.current.contentWindow, highlightTextInput.trim(), { page } ); // 更新上一次高亮的文本 setPreviousHighlightText(highlightTextInput.trim()); const pageInfo = page ? ` (第${page}页)` : ''; setHighlightResult(`✓ 已切换高亮: ${highlightTextInput.trim()}${pageInfo}`); } 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 = () => { if (!iframeRef.current?.contentWindow) { setSearchReplaceResult('iframe 未就绪'); return; } if (!searchText.trim()) { setSearchReplaceResult('请输入搜索文本'); return; } try { unoSearchNext(iframeRef.current.contentWindow, searchText.trim()); setSearchReplaceResult(`✓ 搜索: "${searchText.trim()}"`); // 3秒后清除提示 setTimeout(() => setSearchReplaceResult(null), 3000); } catch (e) { console.error('搜索失败:', e); setSearchReplaceResult(`✗ 搜索失败: ${e instanceof Error ? e.message : '未知错误'}`); } }; // 替换处理函数 const handleReplace = () => { if (!iframeRef.current?.contentWindow) { setSearchReplaceResult('iframe 未就绪'); return; } if (!searchText.trim()) { setSearchReplaceResult('请输入搜索文本'); return; } try { if (replaceAllMode) { unoReplaceAll(iframeRef.current.contentWindow, searchText.trim(), replaceText); setSearchReplaceResult(`✓ 已替换全部: "${searchText.trim()}" → "${replaceText}"`); } else { unoReplaceCurrent(iframeRef.current.contentWindow, searchText.trim(), replaceText); setSearchReplaceResult(`✓ 已替换: "${searchText.trim()}" → "${replaceText}"`); } // 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); 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} )}
{/* 搜索替换测试面板 */}
搜索: setSearchText(e.target.value)} placeholder="输入要搜索的文本" aria-label="搜索文本" onKeyDown={(e) => { if (e.key === 'Enter') { handleSearchNext(); } }} />
替换: 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 */}