diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index b5f9f3c..99f9594 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -14,6 +14,7 @@ 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'; /** * Collabora 文档查看器组件 @@ -37,6 +38,12 @@ export const CollaboraViewer = forwardRef(null); + // 高亮测试面板状态 + const [highlightText, setHighlightText] = useState(''); + const [highlightPage, setHighlightPage] = useState(''); + const [previousHighlightText, setPreviousHighlightText] = useState(null); + const [highlightResult, setHighlightResult] = useState(null); + // 1. 加载 Collabora 配置 const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName); @@ -80,45 +87,86 @@ export const CollaboraViewer = forwardRef { + // 发送 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) { - setUnoResult('iframe 不可用'); + setHighlightResult('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; - } - } + if (!highlightText || highlightText.trim() === '') { + setHighlightResult('请输入要高亮的文本'); + return; } try { - // 使用正确的 sendUnoCommand 方法 - sendUnoCommand(iframeRef.current.contentWindow, unoCmd, args); - setUnoResult(`已发送: ${unoCmd}`); - console.log('[UNO Debug] 发送命令:', unoCmd, args); + // 解析页码 (可选) + const page = highlightPage && highlightPage.trim() !== '' + ? parseInt(highlightPage.trim(), 10) + : undefined; + + // 验证页码 + if (page !== undefined && (isNaN(page) || page < 1)) { + setHighlightResult('页码必须是大于0的整数'); + return; + } + + await switchHighlight( + iframeRef.current.contentWindow, + previousHighlightText, + highlightText.trim(), + { page } + ); + + // 更新上一次高亮的文本 + setPreviousHighlightText(highlightText.trim()); + const pageInfo = page ? ` (第${page}页)` : ''; + setHighlightResult(`✓ 已切换高亮: ${highlightText.trim()}${pageInfo}`); } catch (e) { - console.error('发送 UNO 失败:', e); - setUnoResult('发送失败,请查看控制台'); + console.error('切换高亮失败:', e); + setHighlightResult(`切换失败: ${e instanceof Error ? e.message : '未知错误'}`); } }; return (
{/* UNO 命令测试面板 */} -
+ {/*
{unoResult && {unoResult}} +
*/} + + {/* 高亮测试面板 */} +
+ setHighlightText(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} + )}
{/* 文档加载提示 */} diff --git a/app/components/collabora/lib/Highlightselecttext.ts b/app/components/collabora/lib/Highlightselecttext.ts new file mode 100644 index 0000000..46c24f2 --- /dev/null +++ b/app/components/collabora/lib/Highlightselecttext.ts @@ -0,0 +1,270 @@ +/** + * Collabora Online 高亮选中文本工具 + * + * 职责: 实现文本的搜索、高亮、清除高亮功能,支持跳转到指定页面 + * + * 核心逻辑: + * 1. 搜索并高亮指定文本 + * 2. 清除指定文本的高亮 + * 3. 切换高亮 (清除旧文本高亮 + 高亮新文本) + * 4. 跳转到指定页面后高亮文本 + * + * @encoding UTF-8 + */ + +import { sendUnoCommand } from "../Uno"; + +/** + * 页面跳转响应接口 + */ +interface GotoPageResponse { + pageNumber: number; + pageIndex: number; + totalPages: number; + currentOffset: number; + targetOffset: number; + scrollDelta: 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', {}); +} + +/** + * 切换高亮文本 (支持指定页面) + * + * @param iframeWindow - Collabora iframe 的 contentWindow + * @param oldText - 上一次高亮的文本 (null 表示首次高亮) + * @param newText - 当前要高亮的文本 + * @param options - 可选配置 + * @param options.color - 高亮颜色 (LibreOffice Decimal 格式), 默认 16776960 = 黄色 + * @param options.page - 可选页码 (从1开始),指定后会先跳转到该页面再高亮 + * + * @example + * // 首次高亮 + * await switchHighlight(iframeWindow, null, '{乙方名称}'); + * + * // 切换到下一个字段 + * await switchHighlight(iframeWindow, '{乙方名称}', '{乙方法定代表人}'); + * + * // 跳转到第2页并高亮 + * await switchHighlight(iframeWindow, null, '{合同编号}', { page: 2 }); + */ +export async function switchHighlight( + iframeWindow: Window, + oldText: string | null, + newText: string, + options?: { color?: number; page?: number } +): Promise { + const color = options?.color || 16776960; + const page = options?.page; + + console.log('[HighlightSelectText] 切换高亮:', { oldText, newText, color, page }); + + // 1. 如果存在旧文本,先清除旧高亮 (在跳转之前清除,确保在旧页面清除) + if (oldText && oldText.trim() !== '') { + console.log('[HighlightSelectText] 清除旧高亮:', oldText); + removeHighlight(iframeWindow, oldText); + escapeSelection(iframeWindow); + // 等待清除命令执行完成 + await new Promise(resolve => setTimeout(resolve, 100)); + } + + // 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; + } + } + + // 3. 搜索并高亮新文本 + if (newText && newText.trim() !== '') { + console.log('[HighlightSelectText] 高亮新文本:', newText); + highlightText(iframeWindow, newText, color); + + // 4. 取消选中,避免影响用户后续操作 + escapeSelection(iframeWindow); + } + + console.log('[HighlightSelectText] 切换完成'); +}