From 61facf5d71164ef4b13353ab1f1962694b6e25f1 Mon Sep 17 00:00:00 2001 From: PingChuan <1259732256@qq.com> Date: Fri, 28 Nov 2025 15:44:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E7=BB=84=E8=A3=85UNO=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=89=B9=E5=AE=9A=E9=A1=B5=E9=9D=A2=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=9B=BF=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/collabora/CollaboraViewer.tsx | 377 ++++++++++++++++-- .../collabora/lib/SearchandReplace.ts | 306 ++++++++++++++ app/components/collabora/lib/index.ts | 14 + app/components/collabora/lib/zoom.ts | 37 -- 4 files changed, 656 insertions(+), 78 deletions(-) create mode 100644 app/components/collabora/lib/SearchandReplace.ts delete mode 100644 app/components/collabora/lib/zoom.ts diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index bf7df86..aaa6f42 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -16,7 +16,18 @@ import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from '. import { sendUnoCommand } from './Uno'; import { highlightText } from './lib/Highlightselecttext'; import { clearHighlights } from './lib/ClearHighlight'; -import { unoScrollToTop, requestPageInfo, customGotoPage, type PageInfo, type GotoPageResponse } from './lib'; +import { + unoScrollToTop, + requestPageInfo, + customGotoPage, + unoSearchNext, + unoReplaceCurrent, + unoReplaceAll, + unoCancelSearch, + replaceTextInPage, + type PageInfo, + type GotoPageResponse +} from './lib'; /** * Collabora 文档查看器组件 @@ -54,6 +65,24 @@ export const CollaboraViewer = forwardRef(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); @@ -98,39 +127,39 @@ export const CollaboraViewer = forwardRef { - // if (!iframeRef.current?.contentWindow) { - // setUnoResult('iframe 不可用'); - // return; - // } + 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; - // } - // } - // } + 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('发送失败,请查看控制台'); - // } - // }; + 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 () => { @@ -308,32 +337,164 @@ export const CollaboraViewer = forwardRef { + 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 命令" + placeholder="UNO 命令 (如 .uno:Bold)" aria-label="UNO 命令" + onKeyPress={(e) => { + if (e.key === 'Enter') { + sendUno(); + } + }} /> setUnoArgs(e.target.value)} - placeholder="UNO Args (JSON)" + placeholder='Args (JSON, 如 {"SearchItem.SearchString":{"type":"string","value":"test"}})' aria-label="UNO Args (JSON)" + onKeyPress={(e) => { + if (e.key === 'Enter') { + sendUno(); + } + }} /> - {unoResult && {unoResult}} -
*/} + {unoResult && ( + + {unoResult} + + )} +
{/* 高亮测试面板 */}
@@ -404,6 +565,140 @@ export const CollaboraViewer = forwardRef + {/* 搜索替换测试面板 */} +
+
+ 搜索: + 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 && (
diff --git a/app/components/collabora/lib/SearchandReplace.ts b/app/components/collabora/lib/SearchandReplace.ts new file mode 100644 index 0000000..c2af74c --- /dev/null +++ b/app/components/collabora/lib/SearchandReplace.ts @@ -0,0 +1,306 @@ +/** + * Collabora Online 搜索和替换功能 + * + * 职责: 封装 UNO 搜索替换命令 + * + * @encoding UTF-8 + */ + +import { sendUnoCommand } from '../Uno'; + +/** + * 搜索命令类型 + */ +export enum SearchCommand { + /** 查找下一个 */ + FindNext = 0, + /** 查找上一个 */ + FindPrevious = 1, + /** 替换当前选中 */ + Replace = 2, + /** 替换全部 */ + ReplaceAll = 3, +} + +/** + * 搜索选项 + */ +export interface SearchOptions { + /** 区分大小写 */ + caseSensitive?: boolean; + /** 全字匹配 */ + wholeWord?: boolean; + /** 使用正则表达式 */ + regexp?: boolean; + /** 向后搜索 */ + backward?: boolean; + /** 静默模式(不显示搜索结果提示) */ + quiet?: boolean; +} + +/** + * 替换选项 + */ +export interface ReplaceOptions extends SearchOptions { + /** 替换全部 */ + replaceAll?: boolean; +} + +/** + * 搜索文本 - 查找下一个匹配项 + * @param iframeWindow - iframe 的 contentWindow + * @param text - 要搜索的文本 + * @param options - 搜索选项 + */ +export function unoSearchNext( + iframeWindow: Window, + text: string, + options: SearchOptions = {} +): void { + const { + caseSensitive = false, + wholeWord = false, + regexp = false, + backward = false, + quiet = true, + } = options; + + // 计算 SearchFlags + let searchFlags = 0; + if (wholeWord) searchFlags |= 0x00010000; // SEARCH_WORD + + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: text }, + 'SearchItem.Command': { type: 'long', value: backward ? SearchCommand.FindPrevious : SearchCommand.FindNext }, + 'SearchItem.Backward': { type: 'boolean', value: backward }, + 'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive }, + 'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp }, + 'SearchItem.SearchFlags': { type: 'long', value: searchFlags }, + 'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 }, + 'Quiet': { type: 'boolean', value: quiet }, + }); + + console.log('[SearchReplace] 搜索下一个:', text, options); +} + +/** + * 搜索文本 - 查找上一个匹配项 + * @param iframeWindow - iframe 的 contentWindow + * @param text - 要搜索的文本 + * @param options - 搜索选项 + */ +export function unoSearchPrevious( + iframeWindow: Window, + text: string, + options: SearchOptions = {} +): void { + unoSearchNext(iframeWindow, text, { ...options, backward: true }); + console.log('[SearchReplace] 搜索上一个:', text); +} + +/** + * 替换当前选中的匹配项 + * @param iframeWindow - iframe 的 contentWindow + * @param searchText - 要搜索的文本 + * @param replaceText - 替换后的文本 + * @param options - 替换选项 + */ +export function unoReplaceCurrent( + iframeWindow: Window, + searchText: string, + replaceText: string, + options: SearchOptions = {} +): void { + const { + caseSensitive = false, + wholeWord = false, + regexp = false, + quiet = true, + } = options; + + let searchFlags = 0; + if (wholeWord) searchFlags |= 0x00010000; + + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: searchText }, + 'SearchItem.ReplaceString': { type: 'string', value: replaceText }, + 'SearchItem.Command': { type: 'long', value: SearchCommand.Replace }, + 'SearchItem.Backward': { type: 'boolean', value: false }, + 'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive }, + 'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp }, + 'SearchItem.SearchFlags': { type: 'long', value: searchFlags }, + 'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 }, + 'Quiet': { type: 'boolean', value: quiet }, + }); + + console.log('[SearchReplace] 替换当前:', searchText, '->', replaceText); +} + +/** + * 替换所有匹配项 + * @param iframeWindow - iframe 的 contentWindow + * @param searchText - 要搜索的文本 + * @param replaceText - 替换后的文本 + * @param options - 替换选项 + */ +export function unoReplaceAll( + iframeWindow: Window, + searchText: string, + replaceText: string, + options: SearchOptions = {} +): void { + const { + caseSensitive = false, + wholeWord = false, + regexp = false, + quiet = true, + } = options; + + let searchFlags = 0; + if (wholeWord) searchFlags |= 0x00010000; + + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: searchText }, + 'SearchItem.ReplaceString': { type: 'string', value: replaceText }, + 'SearchItem.Command': { type: 'long', value: SearchCommand.ReplaceAll }, + 'SearchItem.SearchAll': { type: 'boolean', value: true }, + 'SearchItem.Backward': { type: 'boolean', value: false }, + 'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive }, + 'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp }, + 'SearchItem.SearchFlags': { type: 'long', value: searchFlags }, + 'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 }, + 'Quiet': { type: 'boolean', value: quiet }, + }); + + console.log('[SearchReplace] 替换全部:', searchText, '->', replaceText); +} + +/** + * 统一的搜索替换接口 + * @param iframeWindow - iframe 的 contentWindow + * @param searchText - 要搜索的文本 + * @param replaceText - 替换后的文本(可选,不提供则只搜索) + * @param options - 选项 + */ +export function unoSearchAndReplace( + iframeWindow: Window, + searchText: string, + replaceText?: string, + options: ReplaceOptions = {} +): void { + if (replaceText === undefined || replaceText === null) { + // 只搜索 + unoSearchNext(iframeWindow, searchText, options); + } else if (options.replaceAll) { + // 替换全部 + unoReplaceAll(iframeWindow, searchText, replaceText, options); + } else { + // 替换当前 + unoReplaceCurrent(iframeWindow, searchText, replaceText, options); + } +} + +/** + * 取消搜索选中状态 + * @param iframeWindow - iframe 的 contentWindow + */ +export function unoCancelSearch(iframeWindow: Window): void { + sendUnoCommand(iframeWindow, '.uno:Escape', {}); + console.log('[SearchReplace] 取消搜索'); +} + +/** + * 在指定页面替换文本(组合命令) + * 流程:跳转页面 -> 等待 -> 搜索选中 -> 等待 -> 替换 + * + * @param iframeWindow - iframe 的 contentWindow + * @param pageNumber - 目标页码(从1开始) + * @param searchText - 要搜索的文本 + * @param replaceText - 替换后的文本 + * @param options - 搜索选项 + * @returns Promise - 是否成功 + */ +export async function replaceTextInPage( + iframeWindow: Window, + pageNumber: number, + searchText: string, + replaceText: string, + options: SearchOptions = {} +): Promise<{ success: boolean; message: string }> { + const { + caseSensitive = false, + wholeWord = false, + regexp = false, + quiet = true, + } = options; + + console.log('[SearchReplace] 开始在第', pageNumber, '页替换:', searchText, '->', replaceText); + + try { + // 1. 发送自定义消息跳转到指定页面 + const gotoMessage = { + MessageId: 'custompostMessage', + SendTime: Date.now(), + Values: { + Command: 'GOTO_PAGE', + Args: { pageNumber }, + }, + }; + iframeWindow.postMessage(JSON.stringify(gotoMessage), '*'); + console.log('[SearchReplace] 步骤1: 跳转到第', pageNumber, '页'); + + // 等待页面跳转完成 + await delay(500); + + // 2. 搜索文本(查找下一个) + let searchFlags = 0; + if (wholeWord) searchFlags |= 0x00010000; + + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: searchText }, + 'SearchItem.Command': { type: 'long', value: SearchCommand.FindNext }, + 'SearchItem.Backward': { type: 'boolean', value: false }, + 'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive }, + 'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp }, + 'SearchItem.SearchFlags': { type: 'long', value: searchFlags }, + 'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 }, + 'Quiet': { type: 'boolean', value: quiet }, + }); + console.log('[SearchReplace] 步骤2: 搜索文本'); + + // 等待搜索完成 + await delay(300); + + // 3. 替换选中的文本 + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: searchText }, + 'SearchItem.ReplaceString': { type: 'string', value: replaceText }, + 'SearchItem.Command': { type: 'long', value: SearchCommand.Replace }, + 'SearchItem.Backward': { type: 'boolean', value: false }, + 'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive }, + 'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp }, + 'SearchItem.SearchFlags': { type: 'long', value: searchFlags }, + 'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 }, + 'Quiet': { type: 'boolean', value: quiet }, + }); + console.log('[SearchReplace] 步骤3: 替换文本'); + + return { + success: true, + message: `已在第${pageNumber}页替换: "${searchText}" -> "${replaceText}"`, + }; + } catch (e) { + console.error('[SearchReplace] 替换失败:', e); + return { + success: false, + message: e instanceof Error ? e.message : '未知错误', + }; + } +} + +/** + * 延迟函数 + */ +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/app/components/collabora/lib/index.ts b/app/components/collabora/lib/index.ts index d73a633..7e5188c 100644 --- a/app/components/collabora/lib/index.ts +++ b/app/components/collabora/lib/index.ts @@ -26,3 +26,17 @@ export { type GotoPageResponse } from './gotoPage'; +// 搜索替换功能 +export { + unoSearchNext, + unoSearchPrevious, + unoReplaceCurrent, + unoReplaceAll, + unoSearchAndReplace, + unoCancelSearch, + replaceTextInPage, + SearchCommand, + type SearchOptions, + type ReplaceOptions, +} from './SearchandReplace'; + diff --git a/app/components/collabora/lib/zoom.ts b/app/components/collabora/lib/zoom.ts deleted file mode 100644 index 354af0a..0000000 --- a/app/components/collabora/lib/zoom.ts +++ /dev/null @@ -1,37 +0,0 @@ -// /** -// * Collabora 缩放功能模块 -// * -// * @encoding UTF-8 -// */ - -// import { sendUnoCommand } from '../Uno'; - -// /** -// * 放大文档(固定步长) -// * @param iframeWindow - iframe 的 contentWindow -// */ -// export function unoZoomPlus(iframeWindow: Window): void { -// sendUnoCommand(iframeWindow, '.uno:ZoomPlus', {}); -// } - -// /** -// * 缩小文档(固定步长) -// * @param iframeWindow - iframe 的 contentWindow -// */ -// export function unoZoomMinus(iframeWindow: Window): void { -// sendUnoCommand(iframeWindow, '.uno:ZoomMinus', {}); -// } - -// /** -// * 设置文档缩放比例 -// * @param iframeWindow - iframe 的 contentWindow -// * @param percentage - 缩放百分比(例如:100 表示 100%) -// */ -// export function unoSetZoom(iframeWindow: Window, percentage: number): void { -// sendUnoCommand(iframeWindow, '.uno:Zoom', { -// Zoom: { -// type: 'short', -// value: percentage, -// }, -// }); -// }