diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index 45cb7ba..d9d8c30 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -14,7 +14,6 @@ import { useRef, forwardRef, useImperativeHandle, useState, useEffect } from 're import type { CollaboraViewerProps, CollaboraViewerHandle } from './types'; import { useCollaboraConfig, useDocumentReady, useCollaboraUnoCommands } from './hooks'; import { sendUnoCommand } from './Uno'; -import { highlightText as performTextHighlight } from './lib/Highlightselecttext'; import { clearHighlights } from './lib/ClearHighlight'; import { unoScrollToTop, @@ -25,6 +24,8 @@ import { unoReplaceAll, unoCancelSearch, replaceTextInPage, + unoHighlightText, + unoClearHighlight, type PageInfo, type GotoPageResponse } from './lib'; @@ -184,6 +185,7 @@ export const CollaboraViewer = forwardRef { // 如果文档未加载完成,不执行跳转和高亮 if (!isDocumentLoaded || !iframeRef.current?.contentWindow) { @@ -193,27 +195,32 @@ export const CollaboraViewer = forwardRef { - try { - const iframeWindow = iframeRef.current!.contentWindow!; - const textToHighlight = highlightText.trim(); + const iframeWindow = iframeRef.current!.contentWindow!; + const textToHighlight = highlightText.trim(); - // 🔥 在高亮新内容之前,先清除之前的所有高亮 - // console.log('[CollaboraViewer] 清除旧高亮...'); + try { + // 步骤1:清除之前的所有高亮(调用 Python 脚本) + console.log('[CollaboraViewer] 步骤1:清除旧高亮...'); await clearHighlights(iframeWindow, { color: 16776960, // 黄色 timeout: 5000, }); - // 短暂延迟后执行新高亮,确保清除操作完成 + // 短暂延迟,确保清除操作完成 await new Promise(resolve => setTimeout(resolve, 100)); - // 执行新高亮 - await performTextHighlight( - iframeWindow, - textToHighlight, - { page: targetPage } - ); - // console.log(`[CollaboraViewer] 已高亮文本: "${textToHighlight}"${targetPage ? ` (第${targetPage}页)` : ''}`); + // 步骤2:使用 UNO 命令高亮新文本(搜索 + 设置背景色) + console.log(`[CollaboraViewer] 步骤2:高亮文本 "${textToHighlight}"`); + unoHighlightText(iframeWindow, textToHighlight, 16776960); // 黄色 + + // 短暂延迟,确保高亮操作完成 + await new Promise(resolve => setTimeout(resolve, 100)); + + // 步骤3:取消选中状态(避免高亮后文本仍被选中) + console.log('[CollaboraViewer] 步骤3:取消选中状态...'); + sendUnoCommand(iframeWindow, '.uno:Escape', {}); + + console.log(`[CollaboraViewer] ✓ 高亮完成: "${textToHighlight}"`); } catch (error) { console.error('[CollaboraViewer] 高亮失败:', error); } @@ -319,9 +326,10 @@ export const CollaboraViewer = forwardRef { - if (shouldAutoReplaceRef.current && searchText && replaceText && searchReplacePageNumber && isDocumentLoaded) { - console.log('[CollaboraViewer] 静默替换模式:自动执行替换:', { searchText, replaceText, searchReplacePageNumber }); + if (shouldAutoReplaceRef.current && searchText && replaceText && isDocumentLoaded) { + console.log('[CollaboraViewer] 静默替换模式:自动执行替换:', { searchText, replaceText }); // 重置标志 shouldAutoReplaceRef.current = false; @@ -334,30 +342,18 @@ export const CollaboraViewer = forwardRef setTimeout(resolve, 300)); - - // 步骤2:搜索文本(确保文本被选中) - console.log(`[CollaboraViewer] 步骤2:搜索文本 "${searchText}"`); + // 步骤1:搜索文本(确保文本被选中,会自动跳转到匹配位置) + console.log(`[CollaboraViewer] 步骤1:搜索文本 "${searchText}"`); unoSearchNext(iframeRef.current.contentWindow, searchText); // 等待搜索完成 await new Promise(resolve => setTimeout(resolve, 300)); - // 步骤3:执行替换 - console.log(`[CollaboraViewer] 步骤3:替换为 "${replaceText}"`); + // 步骤2:执行替换(替换后光标保留在当前位置) + console.log(`[CollaboraViewer] 步骤2:替换为 "${replaceText}"`); unoReplaceCurrent(iframeRef.current.contentWindow, searchText, replaceText); console.log('[CollaboraViewer] ✓ 静默替换完成'); - - // 显示成功提示(可选) - // toastService.success(`已替换: "${searchText}" → "${replaceText}"`); } catch (error) { console.error('[CollaboraViewer] 静默替换失败:', error); } @@ -365,7 +361,7 @@ export const CollaboraViewer = forwardRef clearTimeout(timer); } - }, [searchText, replaceText, searchReplacePageNumber, isDocumentLoaded]); // eslint-disable-line react-hooks/exhaustive-deps + }, [searchText, replaceText, isDocumentLoaded]); // eslint-disable-line react-hooks/exhaustive-deps // 加载中状态 if (loading) { diff --git a/app/components/collabora/lib/SearchandReplace.ts b/app/components/collabora/lib/SearchandReplace.ts index c2af74c..17f0843 100644 --- a/app/components/collabora/lib/SearchandReplace.ts +++ b/app/components/collabora/lib/SearchandReplace.ts @@ -304,3 +304,66 @@ export async function replaceTextInPage( function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } + +/** + * 高亮文本(使用 UNO 命令) + * 流程:先搜索选中所有匹配项,再设置背景色 + * + * @param iframeWindow - iframe 的 contentWindow + * @param text - 要高亮的文本 + * @param color - 高亮颜色,默认 16776960 = 黄色 + */ +export function unoHighlightText( + iframeWindow: Window, + text: string, + color: number = 16776960 +): void { + // 1. 查找所有匹配项(FindAll) + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: text }, + 'SearchItem.Command': { type: 'long', value: 1 }, // 1 = FindAll / Search Next + 'SearchItem.SearchFlags': { type: 'long', value: 0 }, + 'SearchItem.AlgorithmType': { type: 'short', value: 0 }, + 'SearchItem.Backward': { type: 'boolean', value: false }, + 'SearchItem.SearchCaseSensitive': { type: 'boolean', value: false }, + 'Quiet': { type: 'boolean', value: true }, + }); + + // 2. 设置背景色高亮 + sendUnoCommand(iframeWindow, '.uno:BackColor', { + BackColor: { type: 'long', value: color }, + }); + + console.log('[SearchReplace] 高亮文本:', text, '颜色:', color); +} + +/** + * 清除高亮(使用 UNO 命令) + * 通过设置背景色为透明来清除高亮 + * + * @param iframeWindow - iframe 的 contentWindow + * @param text - 要清除高亮的文本(可选,不传则清除当前选中) + */ +export function unoClearHighlight( + iframeWindow: Window, + text?: string +): void { + if (text) { + // 先搜索选中文本 + sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', { + 'SearchItem.SearchString': { type: 'string', value: text }, + 'SearchItem.Command': { type: 'long', value: 1 }, + 'SearchItem.SearchFlags': { type: 'long', value: 0 }, + 'SearchItem.AlgorithmType': { type: 'short', value: 0 }, + 'SearchItem.Backward': { type: 'boolean', value: false }, + 'Quiet': { type: 'boolean', value: true }, + }); + } + + // 设置背景色为透明(-1 表示无背景色) + sendUnoCommand(iframeWindow, '.uno:BackColor', { + BackColor: { type: 'long', value: -1 }, + }); + + console.log('[SearchReplace] 清除高亮:', text || '当前选中'); +} diff --git a/app/components/collabora/lib/index.ts b/app/components/collabora/lib/index.ts index b958682..8a5a60b 100644 --- a/app/components/collabora/lib/index.ts +++ b/app/components/collabora/lib/index.ts @@ -41,6 +41,8 @@ export { unoSearchAndReplace, unoCancelSearch, replaceTextInPage, + unoHighlightText, + unoClearHighlight, SearchCommand, type SearchOptions, type ReplaceOptions, diff --git a/app/components/contracts/PlaceholderForm.tsx b/app/components/contracts/PlaceholderForm.tsx index 07eba70..ed3ba98 100644 --- a/app/components/contracts/PlaceholderForm.tsx +++ b/app/components/contracts/PlaceholderForm.tsx @@ -3,9 +3,9 @@ * 用于合同起草时填写占位符值 */ -import { useState, useEffect } from 'react'; -import type { PlaceholderSchema } from '~/types/contract-draft'; +import { useEffect, useState } from 'react'; import { messageService } from '~/components/ui/MessageModal'; +import type { PlaceholderSchema } from '~/types/contract-draft'; interface PlaceholderFormProps { schema: PlaceholderSchema | null; @@ -28,6 +28,8 @@ export function PlaceholderForm({ }: PlaceholderFormProps) { const [localValues, setLocalValues] = useState>(values); const [replacingFields, setReplacingFields] = useState>(new Set()); + // 【新增】记录当前高亮的字段(只存一个值),避免重复高亮导致焦点被抢 + const [currentHighlightedField, setCurrentHighlightedField] = useState(null); // 同步外部 values 到本地状态 useEffect(() => { @@ -41,11 +43,44 @@ export function PlaceholderForm({ onChange(newValues); }; - // 处理字段聚焦(高亮文档中的占位符) - const handleFieldFocus = (key: string) => { + // 处理字段点击(高亮文档中的占位符) + const handleFieldClick = async (e: React.MouseEvent, key: string) => { + // 1. 检查是否已经高亮当前字段 + if (currentHighlightedField === key) { + console.log(`[PlaceholderForm] 字段 "${key}" 已高亮,跳过高亮操作`); + return; + } + + // 2. 捕获当前输入框 DOM 元素 + const inputElement = e.currentTarget; + + // 3. 更新当前高亮字段(只保存一个) + setCurrentHighlightedField(key); + console.log(`[PlaceholderForm] 切换高亮字段到 "${key}"`); + + // 4. 调用父组件的高亮回调 if (onFieldFocus) { onFieldFocus(key); } + + // 5. 【核心】延迟后强制夺回焦点(UNO 命令会让 iframe 抢焦点) + // 分多次确保焦点回到输入框,防止被 iframe 再次抢走 + const refocusWithRetry = () => { + console.log(`[PlaceholderForm] 夺回焦点到输入框 "${key}"`); + inputElement.focus(); + + // 保持光标在文字最后 + const len = inputElement.value.length; + if ('setSelectionRange' in inputElement) { + inputElement.setSelectionRange(len, len); + } + }; + + // 第一次夺回焦点:150ms(高亮操作完成时) + setTimeout(refocusWithRetry, 150); + + // 第二次确认焦点:300ms(确保没有被再次抢走) + setTimeout(refocusWithRetry, 300); }; // 处理单个字段替换 @@ -125,11 +160,10 @@ export function PlaceholderForm({