diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index c181edc..a899e7f 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -51,6 +51,7 @@ export const CollaboraViewer = forwardRef iframeRef.current?.contentWindow || null, }), [unoCommands, isDocumentLoaded, mode]); // 5. 将 sendUnoCommand 挂载到 window 对象,供调试面板和控制台使用 @@ -121,24 +122,12 @@ export const CollaboraViewer = forwardRef {/* UNO 命令测试面板 */} -
+ {/*
{unoResult && {unoResult}} -
+
*/} {/* 文档加载提示 */} {!isDocumentLoaded && ( diff --git a/app/components/collabora/Uno.ts b/app/components/collabora/Uno.ts index f67a551..545834f 100644 --- a/app/components/collabora/Uno.ts +++ b/app/components/collabora/Uno.ts @@ -1,7 +1,7 @@ /** - * Collabora Online UNO 命令工具函数 + * Collabora Online UNO 命令核心工具 * - * 职责: 封装 Collabora iframe 的 UNO 命令调用 + * 职责: 提供基础的 UNO 命令发送和状态监听功能 * * @encoding UTF-8 */ @@ -29,136 +29,6 @@ export function sendUnoCommand( iframeWindow.postMessage(JSON.stringify(message), '*'); } -/** - * 搜索文本 - * @param iframeWindow - iframe 的 contentWindow - * @param text - 要搜索的文本 - */ -export function unoSearchText(iframeWindow: Window, text: string): void { - sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { - 'SearchItem.SearchString': { type: 'string', value: text }, - 'SearchItem.Command': { type: 'long', value: 1 }, // 1 = Search Next (搜索下一个) - 'SearchItem.Backward': { type: 'boolean', value: false }, - 'SearchItem.Pattern': { type: 'boolean', value: false }, - 'SearchItem.Content': { type: 'boolean', value: false }, - 'SearchItem.AsianOptions': { type: 'boolean', value: false }, - 'SearchItem.AlgorithmType': { type: 'short', value: 0 }, // 普通搜索 - 'SearchItem.SearchFlags': { type: 'long', value: 0 }, - 'SearchItem.Start': { type: 'boolean', value: true }, // 从头开始搜索 - 'SearchItem.Quiet': { type: 'boolean', value: true }, // 静默模式 - }); -} - -/** - * 替换文本 - * @param iframeWindow - iframe 的 contentWindow - * @param searchText - 要搜索的文本 - * @param replaceText - 替换后的文本 - */ -export function unoReplaceText( - iframeWindow: Window, - searchText: string, - replaceText: string -): void { - sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { - 'SearchItem.SearchString': { type: 'string', value: searchText }, - 'SearchItem.ReplaceString': { type: 'string', value: replaceText }, - 'SearchItem.Command': { type: 'long', value: 3 }, // 3 = ReplaceAll - 'SearchItem.AlgorithmType': { type: 'short', value: 0 }, - 'SearchItem.SearchFlags': { type: 'long', value: 0 }, - 'SearchItem.Backward': { type: 'boolean', value: false }, - 'Quiet': { type: 'boolean', value: true }, - }); -} - -/** - * 高亮文本 - * @param iframeWindow - iframe 的 contentWindow - * @param text - 要高亮的文本 - * @param color - 高亮颜色,默认 16776960 = 黄色 - */ -export function unoHighlightText( - 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 - 要移除高亮的文本 - */ -export function unoRemoveHighlight(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 - */ -export function unoEscape(iframeWindow: Window): void { - sendUnoCommand(iframeWindow, '.uno:Escape', {}); -} - -/** - * 滚动到文档开头 (带焦点请求) - * @param iframeWindow - iframe 的 contentWindow - */ -export async function unoScrollToTop(iframeWindow: Window): Promise { - // 1. 先请求 iframe 获取焦点 - const focusMessage = { - MessageId: 'custompostMessage', - Values: { - Command: 'REQUEST_FOCUS', - Args: {}, - }, - }; - console.log('[custompostMessage] 请求焦点 (滚动到顶部)'); - iframeWindow.postMessage(JSON.stringify(focusMessage), '*'); - - // 2. 等待焦点激活 - await new Promise((resolve) => setTimeout(resolve, 100)); - - // 3. 发送滚动命令 - sendUnoCommand(iframeWindow, '.uno:GoToStartOfDoc', {}); -} - -/** - * 保存文档 - * @param iframeWindow - iframe 的 contentWindow - */ -export function unoSave(iframeWindow: Window): void { - sendUnoCommand(iframeWindow, '.uno:Save'); -} - /** * 获取文档状态 (用于检测命令队列完成) * @param iframeWindow - iframe 的 contentWindow @@ -180,64 +50,3 @@ export function unoGetState(iframeWindow: Window): void { console.log('[UNO] 发送 Get_State (.uno:ModifiedStatus) - 等待命令队列执行完成'); iframeWindow.postMessage(JSON.stringify(message), '*'); } - -/** - * 放大文档(固定步长) - * @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, - }, - }); -} - -/** - * 跳转到指定页面 - * @param iframeWindow - iframe 的 contentWindow - * @param pageNumber - 页码(从1开始) - */ -export function unoGotoPage(iframeWindow: Window, pageNumber: number): void { - sendUnoCommand(iframeWindow, '.uno:GotoPage', { - Page: { - type: 'long', - value: pageNumber, - }, - }); -} - -/** - * 跳转到第一页 - * @param iframeWindow - iframe 的 contentWindow - */ -export function unoFirstPage(iframeWindow: Window): void { - sendUnoCommand(iframeWindow, '.uno:FirstPage', {}); -} - -/** - * 跳转到最后一页 - * @param iframeWindow - iframe 的 contentWindow - */ -export function unoLastPage(iframeWindow: Window): void { - sendUnoCommand(iframeWindow, '.uno:LastPage', {}); -} - diff --git a/app/components/collabora/hooks.ts b/app/components/collabora/hooks.ts index d7ef109..79bc2a5 100644 --- a/app/components/collabora/hooks.ts +++ b/app/components/collabora/hooks.ts @@ -27,7 +27,7 @@ import { unoGotoPage, unoFirstPage, unoLastPage, -} from './Uno'; +} from './lib'; import { COLLABORA_URL } from '~/config/api-config'; // ==================== 1. 配置加载 ==================== diff --git a/app/components/collabora/lib/README.md b/app/components/collabora/lib/README.md new file mode 100644 index 0000000..3ee9816 --- /dev/null +++ b/app/components/collabora/lib/README.md @@ -0,0 +1,111 @@ +# Collabora 功能模块说明 + +本目录包含按功能拆分的 Collabora UNO 命令封装模块。 + +## 文件结构 + +``` +lib/ +├── README.md # 本说明文档 +├── index.ts # 统一导出所有功能模块 +├── search.ts # 搜索功能 +├── replace.ts # 替换功能 +├── highlight.ts # 高亮功能 +├── navigation.ts # 导航/跳转功能 +├── zoom.ts # 缩放功能 +└── document.ts # 文档操作 +``` + +## 功能模块 + +### 1. search.ts - 搜索功能 +- `unoSearchText(iframeWindow, text)` - 搜索文本 + +### 2. replace.ts - 替换功能 +- `unoReplaceText(iframeWindow, searchText, replaceText)` - 替换文本 + +### 3. highlight.ts - 高亮功能 +- `unoHighlightText(iframeWindow, text, color)` - 高亮文本 +- `unoRemoveHighlight(iframeWindow, text)` - 移除高亮 +- `unoEscape(iframeWindow)` - 取消(Escape) + +### 4. navigation.ts - 导航/跳转功能 +- `unoScrollToTop(iframeWindow)` - 滚动到文档开头(带焦点请求) +- `unoGotoPage(iframeWindow, pageNumber)` - 跳转到指定页面 +- `unoFirstPage(iframeWindow)` - 跳转到第一页 +- `unoLastPage(iframeWindow)` - 跳转到最后一页 + +### 5. zoom.ts - 缩放功能 +- `unoZoomPlus(iframeWindow)` - 放大文档 +- `unoZoomMinus(iframeWindow)` - 缩小文档 +- `unoSetZoom(iframeWindow, percentage)` - 设置缩放比例 + +### 6. document.ts - 文档操作 +- `unoSave(iframeWindow)` - 保存文档 + +### 7. pageInfo.ts - 页数信息获取 +- `listenPageNumberChanged(iframeWindow, callback)` - 监听文档页数变化事件 +- `requestPageInfo(iframeWindow)` - 请求页数信息(返回 Promise) +- `getPageInfoFromCollabora()` - 从 Collabora 内部直接获取页数(仅 iframe 内部可用) +- `PageInfo` 接口 - 页数信息类型定义 + +## 使用方式 + +### 方式 1: 从统一入口导入(推荐) + +```typescript +import { + unoSearchText, + unoReplaceText, + unoHighlightText, + unoScrollToTop, + unoSave, +} from '~/components/collabora/lib'; +``` + +### 方式 2: 从具体模块导入 + +```typescript +import { unoSearchText } from '~/components/collabora/lib/search'; +import { unoScrollToTop } from '~/components/collabora/lib/navigation'; +``` + +## 核心工具函数 + +核心的命令发送和状态监听函数位于 `../Uno.ts`: + +- `sendUnoCommand(iframeWindow, command, args)` - 发送 UNO 命令 +- `unoGetState(iframeWindow)` - 获取文档状态(用于检测命令队列完成) + +## 设计原则 + +1. **单一职责**: 每个文件只负责一个功能领域 +2. **清晰命名**: 文件名直接反映功能(search, replace, highlight 等) +3. **统一接口**: 所有函数第一个参数都是 `iframeWindow: Window` +4. **依赖注入**: 通过 import `sendUnoCommand` 而不是重复实现 +5. **便于维护**: 功能独立,修改某个模块不影响其他模块 + +## 扩展指南 + +如果需要添加新功能模块: + +1. 在 `lib/` 下创建新文件,如 `lib/print.ts` +2. 实现功能函数,import `sendUnoCommand` from `../Uno` +3. 在 `lib/index.ts` 中添加导出 +4. 在本 README 中补充说明 + +示例: + +```typescript +// lib/print.ts +import { sendUnoCommand } from '../Uno'; + +export function unoPrint(iframeWindow: Window): void { + sendUnoCommand(iframeWindow, '.uno:Print', {}); +} +``` + +```typescript +// lib/index.ts +export { unoPrint } from './print'; +``` diff --git a/app/components/collabora/lib/document.ts b/app/components/collabora/lib/document.ts new file mode 100644 index 0000000..540062f --- /dev/null +++ b/app/components/collabora/lib/document.ts @@ -0,0 +1,15 @@ +/** + * Collabora 文档操作功能模块 + * + * @encoding UTF-8 + */ + +import { sendUnoCommand } from '../Uno'; + +/** + * 保存文档 + * @param iframeWindow - iframe 的 contentWindow + */ +export function unoSave(iframeWindow: Window): void { + sendUnoCommand(iframeWindow, '.uno:Save'); +} diff --git a/app/components/collabora/lib/highlight.ts b/app/components/collabora/lib/highlight.ts new file mode 100644 index 0000000..6ec1ca4 --- /dev/null +++ b/app/components/collabora/lib/highlight.ts @@ -0,0 +1,64 @@ +/** + * Collabora 高亮功能模块 + * + * @encoding UTF-8 + */ + +import { sendUnoCommand } from '../Uno'; + +/** + * 高亮文本 + * @param iframeWindow - iframe 的 contentWindow + * @param text - 要高亮的文本 + * @param color - 高亮颜色,默认 16776960 = 黄色 + */ +export function unoHighlightText( + 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 - 要移除高亮的文本 + */ +export function unoRemoveHighlight(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 + */ +export function unoEscape(iframeWindow: Window): void { + sendUnoCommand(iframeWindow, '.uno:Escape', {}); +} diff --git a/app/components/collabora/lib/index.ts b/app/components/collabora/lib/index.ts new file mode 100644 index 0000000..32f1394 --- /dev/null +++ b/app/components/collabora/lib/index.ts @@ -0,0 +1,34 @@ +/** + * Collabora 功能模块统一导出 + * + * @encoding UTF-8 + */ + +// 搜索功能 +export { unoSearchText } from './search'; + +// 替换功能 +export { unoReplaceText } from './replace'; + +// 高亮功能 +export { unoHighlightText, unoRemoveHighlight, unoEscape } from './highlight'; + +// 导航/跳转功能 +export { + unoScrollToTop, + unoGotoPage, + unoFirstPage, + unoLastPage, +} from './navigation'; + +// 缩放功能 +export { unoZoomPlus, unoZoomMinus, unoSetZoom } from './zoom'; + +// 文档操作 +export { unoSave } from './document'; + +// 页数信息 +export { + requestPageInfo, + type PageInfo, +} from './pageInfo'; diff --git a/app/components/collabora/lib/navigation.ts b/app/components/collabora/lib/navigation.ts new file mode 100644 index 0000000..e592227 --- /dev/null +++ b/app/components/collabora/lib/navigation.ts @@ -0,0 +1,62 @@ +/** + * Collabora 导航/跳转功能模块 + * + * @encoding UTF-8 + */ + +import { sendUnoCommand } from '../Uno'; + +/** + * 滚动到文档开头 (带焦点请求) + * @param iframeWindow - iframe 的 contentWindow + */ +export async function unoScrollToTop(iframeWindow: Window): Promise { + // 1. 先请求 iframe 获取焦点 + const focusMessage = { + MessageId: 'custompostMessage', + Values: { + Command: 'REQUEST_FOCUS', + Args: {}, + }, + }; + console.log('[custompostMessage] 请求焦点 (滚动到顶部)'); + iframeWindow.postMessage(JSON.stringify(focusMessage), '*'); + + // 2. 等待焦点激活 + await new Promise((resolve) => setTimeout(resolve, 100)); + + // 3. 发送滚动命令,要发三次,有时候卡在表格里面就是需要多发几次 + sendUnoCommand(iframeWindow, '.uno:GoToStartOfDoc', {}); + sendUnoCommand(iframeWindow, '.uno:GoToStartOfDoc', {}); + sendUnoCommand(iframeWindow, '.uno:GoToStartOfDoc', {}); +} + +/** + * 跳转到指定页面 + * @param iframeWindow - iframe 的 contentWindow + * @param pageNumber - 页码(从1开始) + */ +export function unoGotoPage(iframeWindow: Window, pageNumber: number): void { + sendUnoCommand(iframeWindow, '.uno:GotoPage', { + Page: { + type: 'long', + value: pageNumber, + }, + }); +} + +/** + * 跳转到第一页 + * @param iframeWindow - iframe 的 contentWindow + */ +export function unoFirstPage(iframeWindow: Window): void { + sendUnoCommand(iframeWindow, '.uno:FirstPage', {}); +} + +/** + * 跳转到最后一页 + * @param iframeWindow - iframe 的 contentWindow + */ +export function unoLastPage(iframeWindow: Window): void { + sendUnoCommand(iframeWindow, '.uno:LastPage', {}); +} diff --git a/app/components/collabora/lib/pageInfo.ts b/app/components/collabora/lib/pageInfo.ts new file mode 100644 index 0000000..e6f3b3b --- /dev/null +++ b/app/components/collabora/lib/pageInfo.ts @@ -0,0 +1,152 @@ +/** + * Collabora 页数信息获取模块 + * + * @encoding UTF-8 + */ + +/** + * 页数信息接口 + */ +export interface PageInfo { + totalPages: number; + currentPage: number; + timestamp: number; +} + +/** + * Collabora PostMessage 数据类型 + */ +interface CollaboraMessageData { + MessageId?: string; + msgId?: string; + // bundle.js _postMessage 发送的格式 + Values?: { + Command?: string; + Status?: string; + totalPages?: number; + currentPage?: number; + timestamp?: number; + pages?: number; + currentPage?: number; + [key: string]: unknown; + }; + // 原始插件返回的格式 + args?: { + Command?: string; + Status?: string; + totalPages?: number; + currentPage?: number; + timestamp?: number; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +/** + * Collabora 全局对象类型 + */ +interface CollaboraApp { + map?: { + _docLayer?: { + _pages?: number; + _currentPage?: number; + [key: string]: unknown; + }; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +/** + * 扩展 Window 类型以包含 Collabora app + */ +interface CollaboraWindow extends Window { + app?: CollaboraApp; +} + +/** + * 解析 PostMessage 数据 + */ +function parseMessageData(data: unknown): CollaboraMessageData { + if (typeof data === 'string') { + return JSON.parse(data) as CollaboraMessageData; + } + return data as CollaboraMessageData; +} + + +/** + * 通过 PostMessage 请求页数信息 + * + * 使用自定义 custompostMessage 插件获取文档页数。 + * 注意: Collabora 的页码是从 0 开始的。 + * + * @param iframeWindow - iframe 的 contentWindow + * @returns Promise,解析为页数信息 + * + * @example + * ```typescript + * const info = await requestPageInfo(iframeWindow); + * console.log(`文档共 ${info.totalPages} 页,当前在第 ${info.currentPage + 1} 页`); + * ``` + */ +export async function requestPageInfo(iframeWindow: Window): 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); + + // bundle.js 的 _postMessage 会将消息转换为: + // { MessageId: 'custompostMessage_Resp', Values: { Command: 'GET_PAGE_INFO', ... } } + // 所以我们需要监听 MessageId 而不是 msgId + if ( + data.MessageId === 'custompostMessage_Resp' && + data.Values?.Command === 'GET_PAGE_INFO' && + data.Values?.Status === 'success' + ) { + clearTimeout(timeout); + cleanup(); + + const info: PageInfo = { + totalPages: data.Values.totalPages || 0, + currentPage: data.Values.currentPage || 0, + timestamp: data.Values.timestamp || Date.now(), + }; + + resolve(info); + } + } catch (error) { + clearTimeout(timeout); + cleanup(); + reject(error); + } + }; + + const cleanup = () => { + window.removeEventListener('message', handleMessage); + }; + + window.addEventListener('message', handleMessage); + + // 发送请求消息 + const message = { + MessageId: 'custompostMessage', + Values: { + Command: 'GET_PAGE_INFO', + Args: {}, + }, + }; + + iframeWindow.postMessage(JSON.stringify(message), '*'); + }); +} + diff --git a/app/components/collabora/lib/replace.ts b/app/components/collabora/lib/replace.ts new file mode 100644 index 0000000..e548c16 --- /dev/null +++ b/app/components/collabora/lib/replace.ts @@ -0,0 +1,29 @@ +/** + * Collabora 替换功能模块 + * + * @encoding UTF-8 + */ + +import { sendUnoCommand } from '../Uno'; + +/** + * 替换文本 + * @param iframeWindow - iframe 的 contentWindow + * @param searchText - 要搜索的文本 + * @param replaceText - 替换后的文本 + */ +export function unoReplaceText( + iframeWindow: Window, + searchText: string, + replaceText: string +): void { + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: searchText }, + 'SearchItem.ReplaceString': { type: 'string', value: replaceText }, + 'SearchItem.Command': { type: 'long', value: 3 }, // 3 = ReplaceAll + 'SearchItem.AlgorithmType': { type: 'short', value: 0 }, + 'SearchItem.SearchFlags': { type: 'long', value: 0 }, + 'SearchItem.Backward': { type: 'boolean', value: false }, + 'Quiet': { type: 'boolean', value: true }, + }); +} diff --git a/app/components/collabora/lib/search.ts b/app/components/collabora/lib/search.ts new file mode 100644 index 0000000..3448c38 --- /dev/null +++ b/app/components/collabora/lib/search.ts @@ -0,0 +1,27 @@ +/** + * Collabora 搜索功能模块 + * + * @encoding UTF-8 + */ + +import { sendUnoCommand } from '../Uno'; + +/** + * 搜索文本 + * @param iframeWindow - iframe 的 contentWindow + * @param text - 要搜索的文本 + */ +export function unoSearchText(iframeWindow: Window, text: string): void { + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: text }, + 'SearchItem.Command': { type: 'long', value: 1 }, // 1 = Search Next (搜索下一个) + 'SearchItem.Backward': { type: 'boolean', value: false }, + 'SearchItem.Pattern': { type: 'boolean', value: false }, + 'SearchItem.Content': { type: 'boolean', value: false }, + 'SearchItem.AsianOptions': { type: 'boolean', value: false }, + 'SearchItem.AlgorithmType': { type: 'short', value: 0 }, // 普通搜索 + 'SearchItem.SearchFlags': { type: 'long', value: 0 }, + 'SearchItem.Start': { type: 'boolean', value: true }, // 从头开始搜索 + 'SearchItem.Quiet': { type: 'boolean', value: true }, // 静默模式 + }); +} diff --git a/app/components/collabora/lib/zoom.ts b/app/components/collabora/lib/zoom.ts new file mode 100644 index 0000000..35c41bb --- /dev/null +++ b/app/components/collabora/lib/zoom.ts @@ -0,0 +1,37 @@ +/** + * 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, + }, + }); +} diff --git a/app/components/collabora/types.ts b/app/components/collabora/types.ts index 72e920c..275c09e 100644 --- a/app/components/collabora/types.ts +++ b/app/components/collabora/types.ts @@ -61,4 +61,6 @@ export interface CollaboraViewerHandle { isReady: boolean; /** 当前模式 */ mode: 'view' | 'edit'; + /** 获取 iframe 的 contentWindow (用于发送 PostMessage) */ + getIframeWindow: () => Window | null; } diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index 352c3ea..a0ec862 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -6,6 +6,7 @@ import { useState, useEffect, useRef, ChangeEvent } from 'react'; import { Document, Page, pdfjs } from 'react-pdf'; import { DOCUMENT_URL } from '~/api/axios-client'; import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer'; +import { requestPageInfo } from '~/components/collabora/lib/pageInfo'; // 设置worker路径为public目录下的worker文件 // 使用已经下载的兼容版本 (pdfjs-dist v2.12.313) @@ -96,6 +97,49 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage const isDocx = fileExtension === 'docx'; const isPdf = fileExtension === 'pdf'; + // DOCX 页数获取: 使用 requestPageInfo 方法 + useEffect(() => { + if (!isDocx) return; + + // console.log('[FilePreview] DOCX 文档加载,尝试获取页数'); + + let intervalCleared = false; + + // 等待 CollaboraViewer 准备就绪 + const checkInterval = setInterval(() => { + if (intervalCleared) return; + + if (!collaboraViewerRef.current?.isReady) { + console.log('[FilePreview] 等待 Collabora 就绪...'); + return; + } + + // console.log('[FilePreview] Collabora 已就绪,尝试获取页数'); + clearInterval(checkInterval); + intervalCleared = true; + + const iframeWindow = collaboraViewerRef.current.getIframeWindow?.(); + if (!iframeWindow) { + console.warn('[FilePreview] 无法获取 iframe window'); + return; + } + + // 使用 requestPageInfo 获取页数 + requestPageInfo(iframeWindow) + .then((info) => { + setNumPages(info.totalPages); + }) + .catch((error) => { + console.warn('[FilePreview] 获取 DOCX 页数失败:', error.message); + }); + }, 500); + + // 清理定时器 + return () => { + clearInterval(checkInterval); + }; + }, [isDocx]); + // 拖拽状态管理 const [dragMode, setDragMode] = useState(false); // 是否处于拖拽模式 const [isDragging, setIsDragging] = useState(false);