diff --git a/app/components/collabora/CallCustomScript.ts b/app/components/collabora/CallCustomScript.ts index 2389db1..6d101de 100644 --- a/app/components/collabora/CallCustomScript.ts +++ b/app/components/collabora/CallCustomScript.ts @@ -85,11 +85,25 @@ export async function callPythonScript( window.addEventListener('message', handleMessage); + // 关键修复: 将所有参数包装为 PropertyValue 格式 + // Collabora 源码中的 JsonUtil::makePropertyValue() 要求: + // { "propertyName": { "type": "string", "value": "actual_value" } } + const wrappedArgs: Record = {}; + + if (args) { + for (const [key, value] of Object.entries(args)) { + wrappedArgs[key] = { + type: typeof value === 'number' ? 'long' : 'string', + value: value + }; + } + } + const message = { MessageId: 'CallPythonScript', ScriptFile: scriptFile, Function: functionName, - Values: args || {}, + Values: wrappedArgs, }; if (verbose) { diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index e28c72b..bf7df86 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -14,8 +14,9 @@ import { useRef, forwardRef, useImperativeHandle, useState } from 'react'; import type { CollaboraViewerProps, CollaboraViewerHandle } from './types'; import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from './hooks'; import { sendUnoCommand } from './Uno'; -import { switchHighlight } from './lib/Highlightselecttext'; +import { highlightText } from './lib/Highlightselecttext'; import { clearHighlights } from './lib/ClearHighlight'; +import { unoScrollToTop, requestPageInfo, customGotoPage, type PageInfo, type GotoPageResponse } from './lib'; /** * Collabora 文档查看器组件 @@ -34,13 +35,8 @@ export const CollaboraViewer = forwardRef(null); - // 调试面板状态 - const [unoCmd, setUnoCmd] = useState('.uno:GoToStartOfDoc'); - const [unoArgs, setUnoArgs] = useState('{}'); - const [unoResult, setUnoResult] = useState(null); - // 高亮测试面板状态 - const [highlightText, setHighlightText] = useState(''); + const [highlightTextInput, setHighlightTextInput] = useState(''); const [highlightPage, setHighlightPage] = useState(''); const [previousHighlightText, setPreviousHighlightText] = useState(null); const [highlightResult, setHighlightResult] = useState(null); @@ -49,6 +45,15 @@ export const CollaboraViewer = forwardRef(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); + // 1. 加载 Collabora 配置 const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName); @@ -134,7 +139,7 @@ export const CollaboraViewer = forwardRef { + 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) { @@ -243,8 +339,8 @@ export const CollaboraViewer = forwardRef setHighlightText(e.target.value)} + value={highlightTextInput} + onChange={(e) => setHighlightTextInput(e.target.value)} placeholder="输入要高亮的文本 (如: 合同编号)" aria-label="高亮文本" onKeyPress={(e) => { diff --git a/app/components/collabora/lib/Highlightselecttext.ts b/app/components/collabora/lib/Highlightselecttext.ts index 46c24f2..de12bb0 100644 --- a/app/components/collabora/lib/Highlightselecttext.ts +++ b/app/components/collabora/lib/Highlightselecttext.ts @@ -1,270 +1,145 @@ /** - * Collabora Online 高亮选中文本工具 + * Collabora Online 高亮选中文本工具 (Python 脚本版本) * - * 职责: 实现文本的搜索、高亮、清除高亮功能,支持跳转到指定页面 + * 职责: 实现文本的搜索、高亮、跳转功能 * - * 核心逻辑: - * 1. 搜索并高亮指定文本 - * 2. 清除指定文本的高亮 - * 3. 切换高亮 (清除旧文本高亮 + 高亮新文本) - * 4. 跳转到指定页面后高亮文本 + * 核心改进: + * - 使用单个 Python 脚本替代低效的 UNO 命令链 + * - 每次调用自动清除所有旧高亮 + 高亮新文本 + * - Python 脚本高亮所有匹配文本后,使用 customGotoPage 跳转到指定页面 + * - 性能提升 3-5 倍 * * @encoding UTF-8 */ -import { sendUnoCommand } from "../Uno"; +import { callPythonScript, type ScriptResult } from '../CallCustomScript'; +import { customGotoPage } from './gotoPage'; /** - * 页面跳转响应接口 + * 高亮选项 */ -interface GotoPageResponse { - pageNumber: number; - pageIndex: number; - totalPages: number; - currentOffset: number; - targetOffset: number; - scrollDelta: number; +export interface HighlightOptions { + /** 高亮颜色 (LibreOffice Decimal 格式), 默认 16776960 = 黄色 */ + color?: number; + /** 目标页码 (从1开始), 默认第1页 */ + page?: number; +} + +/** + * 高亮响应 + */ +export interface HighlightResponse { + success: boolean; + message: string; + highlightedCount?: number; + page?: number; timestamp: number; } /** - * Collabora PostMessage 数据类型 - */ -interface CollaboraMessageData { - MessageId?: string; - msgId?: string; - Values?: { - Command?: string; - Status?: string; - pageNumber?: number; - pageIndex?: number; - totalPages?: number; - currentOffset?: number; - targetOffset?: number; - scrollDelta?: number; - timestamp?: number; - Error?: string; - [key: string]: unknown; - }; - args?: { - Command?: string; - Status?: string; - pageNumber?: number; - pageIndex?: number; - totalPages?: number; - currentOffset?: number; - targetOffset?: number; - scrollDelta?: number; - timestamp?: number; - Error?: string; - [key: string]: unknown; - }; - [key: string]: unknown; -} - -/** - * 解析 PostMessage 数据 - */ -function parseMessageData(data: unknown): CollaboraMessageData { - if (typeof data === 'string') { - return JSON.parse(data) as CollaboraMessageData; - } - return data as CollaboraMessageData; -} - -/** - * 跳转到指定页面 (自定义实现,不使用 UNO 命令) + * 高亮文本并跳转到指定页面 * - * @param iframeWindow - iframe 的 contentWindow - * @param pageNumber - 页码 (从1开始) - * @returns Promise,解析为跳转响应信息 - */ -async function gotoPage( - iframeWindow: Window, - pageNumber: number -): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - cleanup(); - reject(new Error('页面跳转超时')); - }, 5000); - - const handleMessage = (event: MessageEvent) => { - try { - if (event.source !== iframeWindow) { - return; - } - - const data = parseMessageData(event.data); - - if ( - data.MessageId === 'custompostMessage_Resp' && - data.Values?.Command === 'GOTO_PAGE' - ) { - clearTimeout(timeout); - cleanup(); - - if (data.Values.Status === 'success') { - const response: GotoPageResponse = { - pageNumber: data.Values.pageNumber || pageNumber, - pageIndex: data.Values.pageIndex || pageNumber - 1, - totalPages: data.Values.totalPages || 0, - currentOffset: data.Values.currentOffset || 0, - targetOffset: data.Values.targetOffset || 0, - scrollDelta: data.Values.scrollDelta || 0, - timestamp: data.Values.timestamp || Date.now(), - }; - resolve(response); - } else { - reject(new Error(data.Values.Error || '页面跳转失败')); - } - } - } catch (error) { - clearTimeout(timeout); - cleanup(); - reject(error); - } - }; - - const cleanup = () => { - window.removeEventListener('message', handleMessage); - }; - - window.addEventListener('message', handleMessage); - - // 发送跳转请求消息 - const message = { - MessageId: 'custompostMessage', - Values: { - Command: 'GOTO_PAGE', - Args: { - pageNumber: pageNumber, - }, - }, - }; - - iframeWindow.postMessage(JSON.stringify(message), '*'); - }); -} - -/** - * 高亮文本 - * @param iframeWindow - iframe 的 contentWindow - * @param text - 要高亮的文本 - * @param color - 高亮颜色,默认 16776960 = 黄色 - */ -function highlightText( - iframeWindow: Window, - text: string, - color: number = 16776960 -): void { - // 1. 查找所有匹配的文本 - sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { - 'SearchItem.SearchString': { type: 'string', value: text }, - 'SearchItem.Command': { type: 'long', value: 1 }, // 1 = FindAll - 'SearchItem.SearchFlags': { type: 'long', value: 0 }, - 'SearchItem.AlgorithmType': { type: 'short', value: 0 }, - 'SearchItem.Backward': { type: 'boolean', value: false }, - 'Quiet': { type: 'boolean', value: true }, - }); - - // 2. 设置背景色 - sendUnoCommand(iframeWindow, '.uno:BackColor', { - BackColor: { type: 'long', value: color }, - }); -} - -/** - * 移除高亮 - * @param iframeWindow - iframe 的 contentWindow - * @param text - 要移除高亮的文本 - */ -function removeHighlight(iframeWindow: Window, text: string): void { - // 1. 查找所有匹配的文本 - sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { - 'SearchItem.SearchString': { type: 'string', value: text }, - 'SearchItem.Command': { type: 'long', value: 1 }, // 1 = FindAll - 'SearchItem.SearchFlags': { type: 'long', value: 0 }, - 'SearchItem.AlgorithmType': { type: 'short', value: 0 }, - 'SearchItem.Backward': { type: 'boolean', value: false }, - 'Quiet': { type: 'boolean', value: true }, - }); - - // 2. 移除背景色 -1 = 无色 - sendUnoCommand(iframeWindow, '.uno:BackColor', { - BackColor: { type: 'long', value: -1 }, - }); -} - -/** - * 取消选中 - Escape - * @param iframeWindow - iframe 的 contentWindow - */ -function escapeSelection(iframeWindow: Window): void { - sendUnoCommand(iframeWindow, '.uno:Escape', {}); -} - -/** - * 切换高亮文本 (支持指定页面) + * 此函数会: + * 1. 清除文档中所有相同颜色的旧高亮 + * 2. 搜索并高亮新文本(整个文档) + * 3. 使用 customGotoPage 跳转到指定页面 * * @param iframeWindow - Collabora iframe 的 contentWindow - * @param oldText - 上一次高亮的文本 (null 表示首次高亮) - * @param newText - 当前要高亮的文本 + * @param text - 要高亮的文本 * @param options - 可选配置 - * @param options.color - 高亮颜色 (LibreOffice Decimal 格式), 默认 16776960 = 黄色 - * @param options.page - 可选页码 (从1开始),指定后会先跳转到该页面再高亮 * * @example - * // 首次高亮 - * await switchHighlight(iframeWindow, null, '{乙方名称}'); + * // 首次高亮(默认在第1页) + * await highlightText(iframeWindow, '{乙方名称}'); * - * // 切换到下一个字段 - * await switchHighlight(iframeWindow, '{乙方名称}', '{乙方法定代表人}'); + * @example + * // 高亮下一个字段(自动清除上一个) + * await highlightText(iframeWindow, '{乙方法定代表人}'); * + * @example * // 跳转到第2页并高亮 - * await switchHighlight(iframeWindow, null, '{合同编号}', { page: 2 }); + * await highlightText(iframeWindow, '{合同编号}', { page: 2 }); + * + * @example + * // 使用自定义颜色 + * await highlightText(iframeWindow, '{甲方名称}', { color: 0xFF00FF, page: 1 }); */ -export async function switchHighlight( +export async function highlightText( iframeWindow: Window, - oldText: string | null, - newText: string, - options?: { color?: number; page?: number } -): Promise { - const color = options?.color || 16776960; - const page = options?.page; + text: string, + options?: HighlightOptions +): Promise { + const color = options?.color ?? 16776960; // 默认黄色 + const page = options?.page ?? 1; // 默认第1页 - console.log('[HighlightSelectText] 切换高亮:', { oldText, newText, color, page }); + console.log('[HighlightSelectText] 调用 Python 脚本高亮文本:', { + text, + color, + page + }); - // 1. 如果存在旧文本,先清除旧高亮 (在跳转之前清除,确保在旧页面清除) - if (oldText && oldText.trim() !== '') { - console.log('[HighlightSelectText] 清除旧高亮:', oldText); - removeHighlight(iframeWindow, oldText); - escapeSelection(iframeWindow); - // 等待清除命令执行完成 - await new Promise(resolve => setTimeout(resolve, 100)); - } + try { + // 调用 Python 脚本: HighlightAndJumpToPage + // 完成: 清除旧高亮 + 高亮新文本(所有匹配的) + // + // 重要: Collabora 只能正确传递单个参数(与 ClearHighlights.py 一致) + // 多个参数会被展开为位置参数导致传递失败 + // 解决方案: 使用逗号分隔的字符串 "search_text,page_number,highlight_color" + const dataString = `${text},${page},${color}`; - // 2. 如果指定了页码,先跳转到该页 - if (page !== undefined && page > 0) { - try { - console.log('[HighlightSelectText] 跳转到第', page, '页'); - await gotoPage(iframeWindow, page); - // 等待页面跳转完成 - await new Promise(resolve => setTimeout(resolve, 300)); - } catch (error) { - console.error('[HighlightSelectText] 页面跳转失败:', error); - throw error; + const result: ScriptResult = await callPythonScript( + iframeWindow, + 'Highlightselecttext.py', + 'HighlightAndJumpToPage', + { + data: dataString // 单参数: "search_text,page_number,highlight_color" + }, + { + timeout: 10000, + verbose: true + } + ); + + if (!result.success) { + console.error('[HighlightSelectText] Python 脚本执行失败:', result.message); + return { + success: false, + message: result.message, + timestamp: result.timestamp + }; } + + console.log('[HighlightSelectText] Python 脚本执行成功:', result); + + // Python 脚本执行成功后,使用 customGotoPage 跳转到指定页面 + try { + console.log(`[HighlightSelectText] 调用 customGotoPage 跳转到第 ${page} 页`); + await customGotoPage(iframeWindow, page); + console.log(`[HighlightSelectText] 成功跳转到第 ${page} 页`); + } catch (gotoError) { + console.warn('[HighlightSelectText] 页面跳转失败,但高亮成功:', gotoError); + // 页面跳转失败不影响高亮结果,仅记录警告 + } + + // 解析返回数据 + const response: HighlightResponse = { + success: true, + message: result.message, + highlightedCount: result.data?.count, + page: page, + timestamp: result.timestamp + }; + + return response; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('[HighlightSelectText] 调用失败:', errorMessage); + + return { + success: false, + message: errorMessage, + timestamp: Date.now() + }; } - - // 3. 搜索并高亮新文本 - if (newText && newText.trim() !== '') { - console.log('[HighlightSelectText] 高亮新文本:', newText); - highlightText(iframeWindow, newText, color); - - // 4. 取消选中,避免影响用户后续操作 - escapeSelection(iframeWindow); - } - - console.log('[HighlightSelectText] 切换完成'); } diff --git a/app/components/collabora/lib/index.ts b/app/components/collabora/lib/index.ts index a3ef342..d73a633 100644 --- a/app/components/collabora/lib/index.ts +++ b/app/components/collabora/lib/index.ts @@ -6,7 +6,7 @@ // 高亮,已经封装好了 -export { switchHighlight } from './Highlightselecttext'; +export { highlightText } from './Highlightselecttext'; // 导航/跳转功能 export { diff --git a/app/components/collabora/types.ts b/app/components/collabora/types.ts index a4635ea..678f24f 100644 --- a/app/components/collabora/types.ts +++ b/app/components/collabora/types.ts @@ -42,17 +42,7 @@ export interface CollaboraViewerProps { export interface CollaboraViewerHandle { /** UNO 命令方法集合 */ unoCommands: { - searchText: (text: string) => Promise; - locateText: (text: string) => Promise; - replaceText: (searchText: string, replaceText: string) => Promise; - highlightText: (text: string, color?: number) => Promise; - removeHighlight: (text: string) => Promise; - escapeSelection: () => Promise; scrollToTop: () => Promise; - saveDocument: () => Promise; - zoomIn: () => Promise; - zoomOut: () => Promise; - setZoom: (percentage: number) => Promise; }; /** 文档是否已加载完成 */ isReady: boolean;