feat:完成清除高亮脚本封装

This commit is contained in:
PingChuan
2025-11-27 16:13:51 +08:00
parent d5827a2146
commit f2714360d3
5 changed files with 361 additions and 3 deletions
@@ -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<string, unknown>,
options?: CallScriptOptions
): Promise<ScriptResult> {
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<string, unknown>;
}>,
options?: CallScriptOptions
): Promise<ScriptResult[]> {
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;
}
+74 -1
View File
@@ -15,6 +15,7 @@ import type { CollaboraViewerProps, CollaboraViewerHandle } from './types';
import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from './hooks'; import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from './hooks';
import { sendUnoCommand } from './Uno'; import { sendUnoCommand } from './Uno';
import { switchHighlight } from './lib/Highlightselecttext'; import { switchHighlight } from './lib/Highlightselecttext';
import { clearHighlights } from './lib/ClearHighlight';
/** /**
* Collabora 文档查看器组件 * Collabora 文档查看器组件
@@ -27,7 +28,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
fileId, fileId,
mode = 'view', mode = 'view',
userId = 'guest', userId = 'guest',
userName = '访客', userName = '',
}, },
ref ref
) { ) {
@@ -44,6 +45,10 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
const [previousHighlightText, setPreviousHighlightText] = useState<string | null>(null); const [previousHighlightText, setPreviousHighlightText] = useState<string | null>(null);
const [highlightResult, setHighlightResult] = useState<string | null>(null); const [highlightResult, setHighlightResult] = useState<string | null>(null);
// 清除高亮测试面板状态
const [clearHighlightResult, setClearHighlightResult] = useState<string | null>(null);
const [isClearing, setIsClearing] = useState(false);
// 1. 加载 Collabora 配置 // 1. 加载 Collabora 配置
const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName); const { config, loading, error } = useCollaboraConfig(fileId, mode, userId, userName);
@@ -163,6 +168,50 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
} }
}; };
// 清除高亮处理函数
const handleClearHighlights = async () => {
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 ( return (
<div className="collabora-viewer relative w-full h-full min-h-[600px]"> <div className="collabora-viewer relative w-full h-full min-h-[600px]">
{/* UNO 命令测试面板 */} {/* UNO 命令测试面板 */}
@@ -235,6 +284,30 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
)} )}
</div> </div>
{/* 清除高亮测试面板 */}
<div className="absolute top-16 left-2 z-50 bg-white bg-opacity-90 px-3 py-2 rounded shadow flex items-center gap-2">
<button
className={`px-4 py-1.5 rounded text-sm font-medium transition-colors ${
isClearing
? 'bg-gray-400 text-white cursor-not-allowed'
: 'bg-red-500 text-white hover:bg-red-600'
}`}
onClick={handleClearHighlights}
disabled={!isDocumentLoaded || isClearing}
>
{isClearing ? '清除中...' : '清除所有高亮'}
</button>
{clearHighlightResult && (
<span className={`text-xs ml-2 ${
clearHighlightResult.startsWith('✓') ? 'text-green-600' :
clearHighlightResult.startsWith('✗') ? 'text-red-600' :
'text-gray-600'
}`}>
{clearHighlightResult}
</span>
)}
</div>
{/* 文档加载提示 */} {/* 文档加载提示 */}
{!isDocumentLoaded && ( {!isDocumentLoaded && (
<div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-90 z-10"> <div className="absolute inset-0 flex items-center justify-center bg-white bg-opacity-90 z-10">
@@ -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<ClearHighlightResponse>
*
* @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<ClearHighlightResponse> {
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(),
};
}
}
+1 -1
View File
@@ -375,7 +375,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
fileId={real_path} fileId={real_path}
mode="edit" mode="edit"
userId={userInfo?.sub || 'guest'} userId={userInfo?.sub || 'guest'}
userName={userInfo?.nick_name || '访客'} userName={userInfo?.nick_name || ''}
/> />
); );
} else { } else {
+1 -1
View File
@@ -32,7 +32,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
const fileId = url.searchParams.get('fileId'); const fileId = url.searchParams.get('fileId');
const mode = (url.searchParams.get('mode') || 'view') as 'view' | 'edit'; const mode = (url.searchParams.get('mode') || 'view') as 'view' | 'edit';
const userId = url.searchParams.get('userId') || userInfo?.sub || 'guest'; 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) { if (!fileId) {