feat:完成Collabora初步集成(返回顶部、文档页数获取)
This commit is contained in:
@@ -51,6 +51,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
unoCommands,
|
unoCommands,
|
||||||
isReady: isDocumentLoaded,
|
isReady: isDocumentLoaded,
|
||||||
mode,
|
mode,
|
||||||
|
getIframeWindow: () => iframeRef.current?.contentWindow || null,
|
||||||
}), [unoCommands, isDocumentLoaded, mode]);
|
}), [unoCommands, isDocumentLoaded, mode]);
|
||||||
|
|
||||||
// 5. 将 sendUnoCommand 挂载到 window 对象,供调试面板和控制台使用
|
// 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 (
|
return (
|
||||||
<div className="collabora-viewer relative w-full h-full min-h-[600px]">
|
<div className="collabora-viewer relative w-full h-full min-h-[600px]">
|
||||||
{/* UNO 命令测试面板 */}
|
{/* 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
|
<input
|
||||||
className="px-2 py-1 border rounded text-sm w-48"
|
className="px-2 py-1 border rounded text-sm w-48"
|
||||||
value={unoCmd}
|
value={unoCmd}
|
||||||
@@ -160,7 +149,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
发送 UNO
|
发送 UNO
|
||||||
</button>
|
</button>
|
||||||
{unoResult && <span className="text-xs text-gray-500 ml-2">{unoResult}</span>}
|
{unoResult && <span className="text-xs text-gray-500 ml-2">{unoResult}</span>}
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* 文档加载提示 */}
|
{/* 文档加载提示 */}
|
||||||
{!isDocumentLoaded && (
|
{!isDocumentLoaded && (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Collabora Online UNO 命令工具函数
|
* Collabora Online UNO 命令核心工具
|
||||||
*
|
*
|
||||||
* 职责: 封装 Collabora iframe 的 UNO 命令调用
|
* 职责: 提供基础的 UNO 命令发送和状态监听功能
|
||||||
*
|
*
|
||||||
* @encoding UTF-8
|
* @encoding UTF-8
|
||||||
*/
|
*/
|
||||||
@@ -29,136 +29,6 @@ export function sendUnoCommand(
|
|||||||
iframeWindow.postMessage(JSON.stringify(message), '*');
|
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
|
* @param iframeWindow - iframe 的 contentWindow
|
||||||
@@ -180,64 +50,3 @@ export function unoGetState(iframeWindow: Window): void {
|
|||||||
console.log('[UNO] 发送 Get_State (.uno:ModifiedStatus) - 等待命令队列执行完成');
|
console.log('[UNO] 发送 Get_State (.uno:ModifiedStatus) - 等待命令队列执行完成');
|
||||||
iframeWindow.postMessage(JSON.stringify(message), '*');
|
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,
|
unoGotoPage,
|
||||||
unoFirstPage,
|
unoFirstPage,
|
||||||
unoLastPage,
|
unoLastPage,
|
||||||
} from './Uno';
|
} from './lib';
|
||||||
import { COLLABORA_URL } from '~/config/api-config';
|
import { COLLABORA_URL } from '~/config/api-config';
|
||||||
|
|
||||||
// ==================== 1. 配置加载 ====================
|
// ==================== 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;
|
isReady: boolean;
|
||||||
/** 当前模式 */
|
/** 当前模式 */
|
||||||
mode: 'view' | 'edit';
|
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, Page, pdfjs } from 'react-pdf';
|
||||||
import { DOCUMENT_URL } from '~/api/axios-client';
|
import { DOCUMENT_URL } from '~/api/axios-client';
|
||||||
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
|
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
|
||||||
|
import { requestPageInfo } from '~/components/collabora/lib/pageInfo';
|
||||||
|
|
||||||
// 设置worker路径为public目录下的worker文件
|
// 设置worker路径为public目录下的worker文件
|
||||||
// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313)
|
// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313)
|
||||||
@@ -96,6 +97,49 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
const isDocx = fileExtension === 'docx';
|
const isDocx = fileExtension === 'docx';
|
||||||
const isPdf = fileExtension === 'pdf';
|
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 [dragMode, setDragMode] = useState(false); // 是否处于拖拽模式
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user