Files

195 lines
5.2 KiB
TypeScript
Raw Permalink 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, useMemo, useState } from 'react';
import { COLLABORA_URL } from '~/config/api-config';
import { toastService } from '../ui/Toast';
import {
unoScrollToTop,
unoReplaceAll
} 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 [config, setConfig] = useState<CollaboraConfig | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!fileId) {
setConfig(null);
setError('文件路径不能为空');
setLoading(false);
return;
}
const controller = new AbortController();
const params = new URLSearchParams({
fileId,
mode,
userId,
userName,
});
async function loadConfig() {
setLoading(true);
setError(null);
setConfig(null);
try {
const response = await fetch(`/api/collabora/config?${params}`, {
method: 'GET',
signal: controller.signal,
headers: {
Accept: 'application/json',
},
});
const result = (await response.json()) as CollaboraConfig | { error?: string };
if (!response.ok || ('error' in result && result.error)) {
const errorMessage =
('error' in result && result.error) || `加载配置失败 (${response.status})`;
setError(errorMessage);
toastService.error(`加载文档配置失败: ${errorMessage}`);
return;
}
setConfig(result as CollaboraConfig);
} catch (err) {
if (controller.signal.aborted) return;
const errorMessage = err instanceof Error ? err.message : '加载配置失败';
setError(errorMessage);
toastService.error(`加载文档配置失败: ${errorMessage}`);
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
void loadConfig();
return () => controller.abort();
}, [fileId, mode, userId, userName]);
return {
config,
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 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]);
/**
* 替换所有匹配项
* @param searchText 要搜索的文本
* @param replaceText 替换后的文本
*/
const replaceAll = useCallback(async (searchText: string, replaceText: string) => {
if (!iframeRef.current?.contentWindow) {
console.warn('[UNO] iframe 不可用');
return;
}
console.log('[UNO] 替换全部:', searchText, '->', replaceText);
await unoReplaceAll(iframeRef.current.contentWindow, searchText, replaceText);
await new Promise((resolve) => setTimeout(resolve, 100));
}, [iframeRef]);
return useMemo(
() => ({
scrollToTop,
replaceAll
}),
[
scrollToTop,
replaceAll
]
);
}