diff --git a/app/components/collabora/CallCustomScript.ts b/app/components/collabora/CallCustomScript.ts new file mode 100644 index 0000000..2389db1 --- /dev/null +++ b/app/components/collabora/CallCustomScript.ts @@ -0,0 +1,199 @@ +/** + * Collabora Online Python 脚本调用工具 + * + * 职责: 统一封装所有 Python 脚本的调用逻辑和响应处理 + * @encoding UTF-8 + */ + +export interface ScriptResult { + success: boolean; + message: string; + data?: { + count?: number; + action?: string; + unit?: string; + [key: string]: unknown; + }; + timestamp: number; +} + +export interface CallScriptOptions { + timeout?: number; + verbose?: boolean; +} + +interface PostMessageResponse { + MessageId?: string; + Values?: { + commandName?: string; + success?: boolean; + result?: string | { type?: string; value?: string }; + }; +} + +export async function callPythonScript( + iframeWindow: Window, + scriptFile: string, + functionName: string, + args?: Record, + options?: CallScriptOptions +): Promise { + const timeout = options?.timeout || 10000; + const verbose = options?.verbose ?? true; + + if (verbose) { + console.log('[CallCustomScript] 调用 Python 脚本:', { scriptFile, functionName, args }); + } + + return new Promise((resolve, reject) => { + + const cleanup = () => { + clearTimeout(timeoutId); + window.removeEventListener('message', handleMessage); + }; + + const timeoutId: NodeJS.Timeout = setTimeout(() => { + cleanup(); + reject(new Error(`Python 脚本调用超时 (${timeout}ms): ${scriptFile}.${functionName}`)); + }, timeout); + + const handleMessage = (event: MessageEvent) => { + try { + if (event.source !== iframeWindow) return; + + const data: PostMessageResponse = typeof event.data === 'string' + ? JSON.parse(event.data) : event.data; + + // 兼容两种 MessageId 格式: + // - 'CallPythonScript_Resp' (预期格式) + // - 'CallPythonScript-Result' (bundle.js 实际发送的格式) + if (data.MessageId !== 'CallPythonScript-Result') return; + + cleanup(); + + if (verbose) { + console.log('[CallCustomScript] 收到 Python 脚本响应:', data); + } + + const result = parseScriptResponse(data, verbose); + resolve(result); + } catch (error) { + cleanup(); + reject(error); + } + }; + + window.addEventListener('message', handleMessage); + + const message = { + MessageId: 'CallPythonScript', + ScriptFile: scriptFile, + Function: functionName, + Values: args || {}, + }; + + if (verbose) { + console.log('[CallCustomScript] 发送 PostMessage:', message); + } + + iframeWindow.postMessage(JSON.stringify(message), '*'); + }); +} + +function parseScriptResponse(data: PostMessageResponse, verbose: boolean): ScriptResult { + const values = data.Values; + + if (!values || typeof values !== 'object') { + throw new Error('响应格式错误: Values 字段缺失或格式不正确'); + } + + let resultValue: string | undefined; + + if (typeof values.result === 'string') { + resultValue = values.result; + } else if (values.result && typeof values.result === 'object') { + resultValue = (values.result as { value?: string }).value; + } + + if (verbose) { + console.log('[CallCustomScript] 解析结果:', { + commandName: values.commandName, + unoSuccess: values.success, + resultRaw: values.result, + resultExtracted: resultValue, + }); + } + + if (values.success === false) { + return { + success: false, + message: resultValue || 'UNO 命令执行失败', + timestamp: Date.now(), + }; + } + + if (typeof resultValue !== 'string') { + throw new Error('Python 脚本返回值格式错误: result 不是字符串'); + } + + return parseStandardResponse(resultValue); +} + +function parseStandardResponse(message: string): ScriptResult { + const timestamp = Date.now(); + + if (message.includes('Error:') || message.toLowerCase().includes('error')) { + return { success: false, message, timestamp }; + } + + const countMatch = message.match(/(\w+)\s+(\d+)\s+(regions?|instances?|items?)/i); + + if (countMatch) { + const count = parseInt(countMatch[2], 10); + return { + success: true, + message, + data: { + count, + action: countMatch[1].toLowerCase(), + unit: countMatch[3].toLowerCase(), + }, + timestamp, + }; + } + + return { success: true, message, timestamp }; +} + +export async function callPythonScriptBatch( + iframeWindow: Window, + calls: Array<{ + scriptFile: string; + functionName: string; + args?: Record; + }>, + options?: CallScriptOptions +): Promise { + const results: ScriptResult[] = []; + + for (const call of calls) { + try { + const result = await callPythonScript( + iframeWindow, + call.scriptFile, + call.functionName, + call.args, + options + ); + results.push(result); + } catch (error) { + results.push({ + success: false, + message: error instanceof Error ? error.message : String(error), + timestamp: Date.now(), + }); + } + } + + return results; +} diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index 99f9594..e28c72b 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -15,6 +15,7 @@ import type { CollaboraViewerProps, CollaboraViewerHandle } from './types'; import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from './hooks'; import { sendUnoCommand } from './Uno'; import { switchHighlight } from './lib/Highlightselecttext'; +import { clearHighlights } from './lib/ClearHighlight'; /** * Collabora 文档查看器组件 @@ -27,7 +28,7 @@ export const CollaboraViewer = forwardRef(null); const [highlightResult, setHighlightResult] = useState(null); + // 清除高亮测试面板状态 + const [clearHighlightResult, setClearHighlightResult] = useState(null); + const [isClearing, setIsClearing] = useState(false); + // 1. 加载 Collabora 配置 const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName); @@ -163,6 +168,50 @@ export const CollaboraViewer = forwardRef { + if (!iframeRef.current?.contentWindow) { + setClearHighlightResult('iframe 未就绪'); + return; + } + + setIsClearing(true); + setClearHighlightResult('正在清除高亮...'); + + try { + const result = await clearHighlights(iframeRef.current.contentWindow, { + color: 16776960, // 黄色 + timeout: 10000, + }); + + if (result.success) { + setClearHighlightResult(`✓ ${result.message}`); + console.log('[CollaboraViewer] 清除高亮成功:', result); + + // 清空之前记录的高亮文本 + setPreviousHighlightText(null); + } else { + setClearHighlightResult(`✗ 清除失败: ${result.message}`); + console.error('[CollaboraViewer] 清除高亮失败:', result); + } + + // 3秒后清除提示 + setTimeout(() => { + setClearHighlightResult(null); + }, 3000); + } catch (e) { + console.error('清除高亮失败:', e); + setClearHighlightResult(`✗ 清除失败: ${e instanceof Error ? e.message : '未知错误'}`); + + // 5秒后清除错误提示 + setTimeout(() => { + setClearHighlightResult(null); + }, 5000); + } finally { + setIsClearing(false); + } + }; + return (
{/* UNO 命令测试面板 */} @@ -235,6 +284,30 @@ export const CollaboraViewer = forwardRef + {/* 清除高亮测试面板 */} +
+ + {clearHighlightResult && ( + + {clearHighlightResult} + + )} +
+ {/* 文档加载提示 */} {!isDocumentLoaded && (
diff --git a/app/components/collabora/lib/ClearHighlight.ts b/app/components/collabora/lib/ClearHighlight.ts new file mode 100644 index 0000000..b85525c --- /dev/null +++ b/app/components/collabora/lib/ClearHighlight.ts @@ -0,0 +1,86 @@ +/** + * Collabora Online 清除高亮工具 + * + * 职责: 通过调用 Python 脚本清除文档中的高亮背景色 + * + * 核心逻辑: + * 1. 使用通用的 callPythonScript 工具调用 ClearHighlights.py + * 2. 将 ScriptResult 转换为业务层的 ClearHighlightResponse + * + * @encoding UTF-8 + */ + +import { callPythonScript, type ScriptResult } from '../CallCustomScript'; + +/** + * 清除高亮响应接口 + */ +export interface ClearHighlightResponse { + success: boolean; + message: string; + count?: number; // 清除的高亮区域数量 + timestamp: number; +} + +/** + * 清除高亮选项 + */ +export interface ClearHighlightOptions { + color?: number; // 要清除的颜色值,默认 16776960 (黄色) + timeout?: number; // 超时时间(毫秒),默认 10000ms +} + +/** + * 清除指定颜色的高亮 + * + * 通过调用 Collabora 服务端的 Python 脚本 (ClearHighlights.py) 来清除文档中所有指定颜色的背景高亮。 + * + * @param iframeWindow - Collabora iframe 的 contentWindow + * @param options - 可选配置 + * @param options.color - 要清除的颜色值 (LibreOffice Decimal 格式), 默认 16776960 = 黄色 + * @param options.timeout - 超时时间(毫秒),默认 10000ms + * @returns Promise + * + * @example + * // 清除默认黄色高亮 + * const result = await clearHighlights(iframeWindow); + * console.log(result.message); // "Success: Cleared 5 regions." + * + * @example + * // 清除自定义颜色高亮 + * const result = await clearHighlights(iframeWindow, { color: 0xFF00FF }); // 紫色 + */ +export async function clearHighlights( + iframeWindow: Window, + options?: ClearHighlightOptions +): Promise { + const color = options?.color ?? 16776960; // 默认黄色 + const timeout = options?.timeout ?? 10000; // 默认10秒超时 + + try { + // 使用通用的 callPythonScript 工具 + const result: ScriptResult = await callPythonScript( + iframeWindow, + 'ClearHighlights.py', + 'ClearSpecificColor', + { color }, + { timeout, verbose: true } + ); + + // 将 ScriptResult 转换为 ClearHighlightResponse + return { + success: result.success, + message: result.message, + count: result.data?.count, + timestamp: result.timestamp, + }; + + } catch (error) { + // 处理超时或其他错误 + return { + success: false, + message: error instanceof Error ? error.message : '清除高亮失败', + timestamp: Date.now(), + }; + } +} diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index 91b5ee2..6c9a719 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -375,7 +375,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage fileId={real_path} mode="edit" userId={userInfo?.sub || 'guest'} - userName={userInfo?.nick_name || '访客'} + userName={userInfo?.nick_name || ''} /> ); } else { diff --git a/app/routes/api.collabora.config.tsx b/app/routes/api.collabora.config.tsx index 09b9277..7c51721 100644 --- a/app/routes/api.collabora.config.tsx +++ b/app/routes/api.collabora.config.tsx @@ -32,7 +32,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const fileId = url.searchParams.get('fileId'); const mode = (url.searchParams.get('mode') || 'view') as 'view' | 'edit'; const userId = url.searchParams.get('userId') || userInfo?.sub || 'guest'; - const userName = url.searchParams.get('userName') || userInfo?.nick_name || '访客'; + const userName = url.searchParams.get('userName') || userInfo?.nick_name || ''; // 验证必需参数 if (!fileId) {