feat:完成Collabora初步集成(返回顶部、文档页数获取)
This commit is contained in:
@@ -51,6 +51,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
||||
unoCommands,
|
||||
isReady: isDocumentLoaded,
|
||||
mode,
|
||||
getIframeWindow: () => iframeRef.current?.contentWindow || null,
|
||||
}), [unoCommands, isDocumentLoaded, mode]);
|
||||
|
||||
// 5. 将 sendUnoCommand 挂载到 window 对象,供调试面板和控制台使用
|
||||
@@ -121,24 +122,12 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 先让 iframe 获得焦点
|
||||
iframeRef.current.focus();
|
||||
console.log('[调试面板] 已聚焦 iframe');
|
||||
|
||||
(window as any).sendUno?.(unoCmd, args);
|
||||
setUnoResult(`已发送: ${unoCmd}`);
|
||||
} catch (e) {
|
||||
console.error('发送 UNO 失败:', e);
|
||||
setUnoResult('发送失败,请查看控制台');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="collabora-viewer relative w-full h-full min-h-[600px]">
|
||||
{/* UNO 命令测试面板 */}
|
||||
<div className="absolute top-2 left-2 z-50 bg-white bg-opacity-90 px-2 py-1 rounded shadow flex items-center gap-2">
|
||||
{/* <div className="absolute top-2 left-2 z-50 bg-white bg-opacity-90 px-2 py-1 rounded shadow flex items-center gap-2">
|
||||
<input
|
||||
className="px-2 py-1 border rounded text-sm w-48"
|
||||
value={unoCmd}
|
||||
@@ -160,7 +149,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
||||
发送 UNO
|
||||
</button>
|
||||
{unoResult && <span className="text-xs text-gray-500 ml-2">{unoResult}</span>}
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{/* 文档加载提示 */}
|
||||
{!isDocumentLoaded && (
|
||||
|
||||
@@ -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<void> {
|
||||
// 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', {});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
unoGotoPage,
|
||||
unoFirstPage,
|
||||
unoLastPage,
|
||||
} from './Uno';
|
||||
} from './lib';
|
||||
import { COLLABORA_URL } from '~/config/api-config';
|
||||
|
||||
// ==================== 1. 配置加载 ====================
|
||||
|
||||
@@ -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';
|
||||
```
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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', {});
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Collabora 导航/跳转功能模块
|
||||
*
|
||||
* @encoding UTF-8
|
||||
*/
|
||||
|
||||
import { sendUnoCommand } from '../Uno';
|
||||
|
||||
/**
|
||||
* 滚动到文档开头 (带焦点请求)
|
||||
* @param iframeWindow - iframe 的 contentWindow
|
||||
*/
|
||||
export async function unoScrollToTop(iframeWindow: Window): Promise<void> {
|
||||
// 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', {});
|
||||
}
|
||||
@@ -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<PageInfo> {
|
||||
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), '*');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -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 }, // 静默模式
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -61,4 +61,6 @@ export interface CollaboraViewerHandle {
|
||||
isReady: boolean;
|
||||
/** 当前模式 */
|
||||
mode: 'view' | 'edit';
|
||||
/** 获取 iframe 的 contentWindow (用于发送 PostMessage) */
|
||||
getIframeWindow: () => Window | null;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user