temp:临时备份,测试合并兼容性

This commit is contained in:
PingChuan
2025-11-20 20:36:42 +08:00
parent 2e604e8ede
commit b9fe57c5fa
12 changed files with 1310 additions and 36 deletions
+284
View File
@@ -0,0 +1,284 @@
/**
* 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,
} 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}"`);
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}"`);
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}"`);
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}"`);
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] 取消选中');
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] 滚动到顶部');
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] 保存文档');
unoSave(iframeRef.current.contentWindow);
await new Promise((resolve) => setTimeout(resolve, 1000));
}, [iframeRef]);
return useMemo(
() => ({
searchText,
locateText,
replaceText,
highlightText,
removeHighlight,
escapeSelection,
scrollToTop,
saveDocument,
}),
[
searchText,
locateText,
replaceText,
highlightText,
removeHighlight,
escapeSelection,
scrollToTop,
saveDocument,
]
);
}