feat:完成清除高亮脚本封装
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user