/** * Collabora Online 相关的自定义 hooks * * 功能: * - useCollaboraConfig: 加载 Collabora 配置(使用 Remix useFetcher) * - useDocumentReady: 监听文档加载完成 * - useCollaboraUnoCommands: UNO 命令封装 * * @encoding UTF-8 */ import { useFetcher } from '@remix-run/react'; import { RefObject, useCallback, useEffect, useMemo, useState } from 'react'; import { COLLABORA_URL } from '~/config/api-config'; import { toastService } from '../ui/Toast'; import { unoScrollToTop, } from './lib'; import type { CollaboraConfig } from './types'; // ==================== 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(); const [error, setError] = useState(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 errorData = fetcher.data as { error: string }; const errorMessage = errorData.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) { 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) { /** * 滚动到文档顶部 */ 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]); return useMemo( () => ({ scrollToTop }), [ scrollToTop ] ); }