From 49a3323d95e9784903fb314c275d7f69356b9aba Mon Sep 17 00:00:00 2001 From: root <295172551@qq.com> Date: Tue, 29 Jul 2025 00:46:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ConfigContentEditor?= =?UTF-8?q?=20=E6=90=9C=E7=B4=A2=E9=AB=98=E4=BA=AE=E5=9B=BE=E5=B1=82?= =?UTF-8?q?=E9=87=8D=E5=8F=A0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除复杂的高亮显示层,改用原生 textarea 选择高亮 - 添加搜索状态指示器和视觉反馈 - 保持所有搜索功能完整(键盘导航、匹配计数等) - 解决图层重叠和滚动同步问题 --- .../src/components/ui/ConfigContentEditor.tsx | 267 +++++++++++++----- 1 file changed, 196 insertions(+), 71 deletions(-) diff --git a/src/X1.WebUI/src/components/ui/ConfigContentEditor.tsx b/src/X1.WebUI/src/components/ui/ConfigContentEditor.tsx index 9b88d40..0dbfbb4 100644 --- a/src/X1.WebUI/src/components/ui/ConfigContentEditor.tsx +++ b/src/X1.WebUI/src/components/ui/ConfigContentEditor.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; -import { MagnifyingGlassIcon, Cross2Icon } from '@radix-ui/react-icons'; +import { MagnifyingGlassIcon, Cross2Icon, UploadIcon, TrashIcon } from '@radix-ui/react-icons'; interface ConfigContentEditorProps { value: string; @@ -27,84 +27,194 @@ export default function ConfigContentEditor({ className = "" }: ConfigContentEditorProps) { const [searchTerm, setSearchTerm] = useState(''); - const [highlightedContent, setHighlightedContent] = useState(''); const [isSearchVisible, setIsSearchVisible] = useState(false); + const [currentMatchIndex, setCurrentMatchIndex] = useState(0); + const [totalMatches, setTotalMatches] = useState(0); const textareaRef = useRef(null); + const fileInputRef = useRef(null); - // 高亮搜索内容 + // 计算搜索匹配项 useEffect(() => { if (!searchTerm.trim()) { - setHighlightedContent(value); + setCurrentMatchIndex(0); + setTotalMatches(0); return; } - const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); - const highlighted = value.replace(regex, '$1'); - setHighlightedContent(highlighted); - }, [value, searchTerm]); + try { + const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(${escapedSearchTerm})`, 'gi'); + const matches = [...value.matchAll(regex)]; + setTotalMatches(matches.length); + + // 确保当前匹配索引在有效范围内 + if (currentMatchIndex >= matches.length) { + setCurrentMatchIndex(0); + } + } catch (error) { + console.error('搜索匹配计算错误:', error); + setTotalMatches(0); + setCurrentMatchIndex(0); + } + }, [value, searchTerm, currentMatchIndex]); - // 搜索下一个匹配项 - const findNext = () => { + // 处理文件读取 + const handleFileRead = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + // 检查文件类型 + const allowedTypes = [ + 'text/plain', + 'application/json', + 'application/xml', + 'text/xml', + 'text/yaml', + 'text/yml', + 'application/yaml', + 'application/yml' + ]; + + if (!allowedTypes.includes(file.type) && !file.name.match(/\.(txt|json|xml|yaml|yml|conf|config|cfg)$/i)) { + alert('请选择文本文件格式(.txt, .json, .xml, .yaml, .yml, .conf, .config, .cfg)'); + return; + } + + // 检查文件大小(限制为5MB) + if (file.size > 5 * 1024 * 1024) { + alert('文件大小不能超过5MB'); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result as string; + if (content) { + onChange(content); + } + }; + reader.onerror = () => { + alert('读取文件失败,请重试'); + }; + reader.readAsText(file, 'UTF-8'); + + // 清空文件输入,允许重复选择同一文件 + event.target.value = ''; + }; + + // 触发文件选择 + const handleFileSelect = () => { + fileInputRef.current?.click(); + }; + + // 清空内容 + const handleClear = () => { + if (confirm('确定要清空所有内容吗?')) { + onChange(''); + setSearchTerm(''); + setIsSearchVisible(false); + } + }; + + // 定位到指定匹配项 + const scrollToMatch = (matchIndex: number) => { if (!searchTerm.trim() || !textareaRef.current) return; const textarea = textareaRef.current; const text = textarea.value; - const currentPos = textarea.selectionStart; - const searchIndex = text.toLowerCase().indexOf(searchTerm.toLowerCase(), currentPos); + const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); + const matches = [...text.matchAll(regex)]; - if (searchIndex !== -1) { - textarea.setSelectionRange(searchIndex, searchIndex + searchTerm.length); + if (matchIndex >= 0 && matchIndex < matches.length) { + const match = matches[matchIndex]; + const startIndex = match.index!; + const endIndex = startIndex + searchTerm.length; + + // 设置选择范围并高亮 + textarea.setSelectionRange(startIndex, endIndex); textarea.focus(); - } else { - // 如果从当前位置没找到,从头开始搜索 - const firstIndex = text.toLowerCase().indexOf(searchTerm.toLowerCase()); - if (firstIndex !== -1) { - textarea.setSelectionRange(firstIndex, firstIndex + searchTerm.length); - textarea.focus(); - } + + // 滚动到可见位置 + const lineHeight = parseInt(getComputedStyle(textarea).lineHeight) || 20; + const linesBeforeMatch = text.substring(0, startIndex).split('\n').length - 1; + const scrollTop = linesBeforeMatch * lineHeight - textarea.clientHeight / 2; + textarea.scrollTop = Math.max(0, scrollTop); + + // 添加临时高亮效果 + textarea.style.backgroundColor = '#fef3c7'; + setTimeout(() => { + textarea.style.backgroundColor = ''; + }, 500); } }; + // 搜索下一个匹配项 + const findNext = () => { + if (!searchTerm.trim()) return; + + const nextIndex = (currentMatchIndex + 1) % totalMatches; + setCurrentMatchIndex(nextIndex); + scrollToMatch(nextIndex); + }; + // 搜索上一个匹配项 const findPrevious = () => { - if (!searchTerm.trim() || !textareaRef.current) return; - - const textarea = textareaRef.current; - const text = textarea.value; - const currentPos = textarea.selectionStart; - - // 从当前位置向前搜索 - const beforeText = text.substring(0, currentPos); - const lastIndex = beforeText.toLowerCase().lastIndexOf(searchTerm.toLowerCase()); + if (!searchTerm.trim()) return; - if (lastIndex !== -1) { - textarea.setSelectionRange(lastIndex, lastIndex + searchTerm.length); - textarea.focus(); - } else { - // 如果从当前位置没找到,从末尾开始搜索 - const lastIndexFromEnd = text.toLowerCase().lastIndexOf(searchTerm.toLowerCase()); - if (lastIndexFromEnd !== -1) { - textarea.setSelectionRange(lastIndexFromEnd, lastIndexFromEnd + searchTerm.length); - textarea.focus(); - } - } + const prevIndex = currentMatchIndex === 0 ? totalMatches - 1 : currentMatchIndex - 1; + setCurrentMatchIndex(prevIndex); + scrollToMatch(prevIndex); }; - // 计算匹配数量 - const getMatchCount = () => { - if (!searchTerm.trim()) return 0; - const regex = new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); - const matches = value.match(regex); - return matches ? matches.length : 0; + // 搜索输入变化时重置匹配索引 + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value); + setCurrentMatchIndex(0); }; - const matchCount = getMatchCount(); + // 键盘快捷键支持 + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!isSearchVisible || !searchTerm.trim()) return; + + if (e.key === 'Enter') { + e.preventDefault(); + if (e.shiftKey) { + findPrevious(); + } else { + findNext(); + } + } + }; return (
+ {/* 清空按钮 */} + + + {/* 文件读取按钮 */} + + {/* 搜索按钮 */}
+ {/* 隐藏的文件输入 */} + + {/* 搜索栏 */} {isSearchVisible && (
setSearchTerm(e.target.value)} + onChange={handleSearchChange} + onKeyDown={handleKeyDown} className="pl-10" autoFocus />
{searchTerm && ( - - {matchCount} 个匹配 + + {totalMatches > 0 ? ( + + {currentMatchIndex + 1} / {totalMatches} + + + ) : '无匹配项'} )}