Files
leaudit-platform-frontend/app/components/collabora/hooks.ts
T
2025-11-22 19:02:53 +08:00

391 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Collabora Online 相关的自定义 hooks
*
* 功能:
* - useCollaboraConfig: 加载 Collabora 配置(使用 Remix useFetcher
* - useDocumentReady: 监听文档加载完成
* - useCollaboraUnoCommands: UNO 命令封装
*
* @encoding UTF-8
*/
import { RefObject, useCallback, useEffect, useState, useMemo } from 'react';
import { useFetcher } from '@remix-run/react';
import { toastService } from '../ui/Toast';
import type { CollaboraConfig } from './types';
import {
unoSearchText,
unoReplaceText,
unoHighlightText,
unoRemoveHighlight,
unoEscape,
unoScrollToTop,
unoSave,
unoZoomPlus,
unoZoomMinus,
unoSetZoom,
unoGotoPage,
unoFirstPage,
unoLastPage,
} from './Uno';
import { COLLABORA_URL } from '~/config/api-config';
// ==================== 1. 配置加载 ====================
/**
* 加载 Collabora 配置(使用 Remix useFetcher
* @param fileId - 文件路径
* @param mode - 模式(view 或 edit
* @param userId - 用户 ID
* @param userName - 用户名
* @returns 配置、加载状态、错误信息
*/
export function useCollaboraConfig(
fileId: string,
mode: 'view' | 'edit',
userId: string,
userName: string
) {
const fetcher = useFetcher<CollaboraConfig>();
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (fetcher.state === 'idle' && !fetcher.data) {
// 构建查询参数
const params = new URLSearchParams({
fileId,
mode,
userId,
userName,
});
// 加载配置
fetcher.load(`/api/collabora/config?${params}`);
}
}, [fileId, mode, userId, userName, fetcher]);
// 检查错误
useEffect(() => {
if (fetcher.data && 'error' in fetcher.data) {
const errorMessage = (fetcher.data as any).error || '加载配置失败';
setError(errorMessage);
toastService.error(`加载文档配置失败: ${errorMessage}`);
}
}, [fetcher.data]);
return {
config: fetcher.data && !('error' in fetcher.data) ? fetcher.data : null,
loading: fetcher.state === 'loading',
error,
};
}
// ==================== 2. 文档加载状态监听 ====================
/**
* 监听文档加载完成
* @param iframeRef - iframe 引用
* @returns 文档加载状态
*/
export function useDocumentReady(iframeRef: RefObject<HTMLIFrameElement>) {
const [isDocumentLoaded, setIsDocumentLoaded] = useState(false);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// 验证消息来源
const collaboraOrigin = new URL(COLLABORA_URL).origin;
if (event.origin !== collaboraOrigin) {
return;
}
try {
const msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
if (msg.MessageId === 'App_LoadingStatus' && msg.Values?.Status === 'Document_Loaded') {
console.log('[DocumentReady] 文档加载完成');
setIsDocumentLoaded(true);
}
} catch (err) {
console.warn('[DocumentReady] 解析消息失败:', err);
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, [iframeRef]);
return { isDocumentLoaded };
}
// ==================== 3. UNO 命令封装 ====================
/**
* UNO 命令封装(React Hook
* @param iframeRef - iframe 引用
* @returns UNO 命令方法集合
*/
export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>) {
/**
* 搜索文本(用于定位)
*/
const searchText = useCallback(
async (text: string) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 搜索文本: "${text}"`);
await unoSearchText(iframeRef.current.contentWindow, text);
await new Promise((resolve) => setTimeout(resolve, 100));
},
[iframeRef]
);
/**
* 定位文本(搜索 + 立即取消选中)
* 用于"只看不改"的场景,避免蓝色选中背景
*/
const locateText = useCallback(
async (text: string) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 定位文本(无选中): "${text}"`);
// 1. 执行搜索(滚动到目标并选中)
await searchText(text);
// 2. 等待渲染完成
await new Promise((resolve) => setTimeout(resolve, 50));
// 3. 取消选中(去除蓝色背景,保留视图位置)
unoEscape(iframeRef.current.contentWindow);
console.log(`[UNO] 定位完成,已取消选中`);
},
[searchText, iframeRef]
);
/**
* 替换文本(ReplaceAll
*/
const replaceText = useCallback(
async (searchText: string, replaceText: string) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 替换文本: "${searchText}" -> "${replaceText}"`);
await unoReplaceText(iframeRef.current.contentWindow, searchText, replaceText);
await new Promise((resolve) => setTimeout(resolve, 200));
},
[iframeRef]
);
/**
* 高亮文本
*/
const highlightText = useCallback(
async (text: string, color?: number) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 高亮文本: "${text}"`);
await unoHighlightText(iframeRef.current.contentWindow, text, color);
await new Promise((resolve) => setTimeout(resolve, 200));
},
[iframeRef]
);
/**
* 移除高亮
*/
const removeHighlight = useCallback(
async (text: string) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 移除高亮: "${text}"`);
await unoRemoveHighlight(iframeRef.current.contentWindow, text);
await new Promise((resolve) => setTimeout(resolve, 200));
},
[iframeRef]
);
/**
* 取消选中(Escape
*/
const escapeSelection = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 取消选中');
await unoEscape(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 50));
}, [iframeRef]);
/**
* 滚动到文档顶部
*/
const scrollToTop = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 滚动到顶部');
await unoScrollToTop(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
/**
* 保存文档
*/
const saveDocument = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 保存文档');
await unoSave(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 1000));
}, [iframeRef]);
/**
* 放大文档
*/
const zoomIn = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 放大文档');
await unoZoomPlus(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
/**
* 缩小文档
*/
const zoomOut = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 缩小文档');
await unoZoomMinus(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
/**
* 设置缩放比例
* @param percentage - 缩放百分比 (例如:100 表示 100%)
*/
const setZoom = useCallback(
async (percentage: number) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 设置缩放比例: ${percentage}%`);
await unoSetZoom(iframeRef.current.contentWindow, percentage);
await new Promise((resolve) => setTimeout(resolve, 100));
},
[iframeRef]
);
/**
* 跳转到指定页面
* @param pageNumber - 页码(从1开始)
*/
const gotoPage = useCallback(
async (pageNumber: number) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log(`[UNO] 跳转到第 ${pageNumber}`);
await unoGotoPage(iframeRef.current.contentWindow, pageNumber);
await new Promise((resolve) => setTimeout(resolve, 100));
},
[iframeRef]
);
/**
* 跳转到第一页
*/
const gotoFirstPage = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 跳转到第一页');
await unoFirstPage(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
/**
* 跳转到最后一页
*/
const gotoLastPage = useCallback(async () => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 跳转到最后一页');
await unoLastPage(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
return useMemo(
() => ({
searchText,
locateText,
replaceText,
highlightText,
removeHighlight,
escapeSelection,
scrollToTop,
saveDocument,
zoomIn,
zoomOut,
setZoom,
gotoPage,
gotoFirstPage,
gotoLastPage,
}),
[
searchText,
locateText,
replaceText,
highlightText,
removeHighlight,
escapeSelection,
scrollToTop,
saveDocument,
zoomIn,
zoomOut,
setZoom,
gotoPage,
gotoFirstPage,
gotoLastPage,
]
);
}