feat:完成Collabora初步集成(返回顶部、文档页数获取)

This commit is contained in:
PingChuan
2025-11-25 10:56:47 +08:00
parent 31614374a7
commit 272c3e8dce
14 changed files with 583 additions and 208 deletions
+3 -14
View File
@@ -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 && (
+2 -193
View File
@@ -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', {});
}
+1 -1
View File
@@ -27,7 +27,7 @@ import {
unoGotoPage,
unoFirstPage,
unoLastPage,
} from './Uno';
} from './lib';
import { COLLABORA_URL } from '~/config/api-config';
// ==================== 1. 配置加载 ====================
+111
View File
@@ -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';
```
+15
View File
@@ -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');
}
+64
View File
@@ -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', {});
}
+34
View File
@@ -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', {});
}
+152
View File
@@ -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), '*');
});
}
+29
View File
@@ -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 },
});
}
+27
View File
@@ -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 }, // 静默模式
});
}
+37
View File
@@ -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,
},
});
}
+2
View File
@@ -61,4 +61,6 @@ export interface CollaboraViewerHandle {
isReady: boolean;
/** 当前模式 */
mode: 'view' | 'edit';
/** 获取 iframe 的 contentWindow (用于发送 PostMessage) */
getIframeWindow: () => Window | null;
}
+44
View File
@@ -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);