diff --git a/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx b/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx index e6712973..6422bc72 100644 --- a/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx +++ b/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx @@ -9,9 +9,16 @@ import { Code, AlertCircle, Download, - Save + Save, + RotateCcw, + Search, + X, + ChevronUp, + ChevronDown, + Replace } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; interface FileItem { name: string; @@ -27,6 +34,7 @@ interface FileItem { interface FileViewerProps { file: FileItem; content?: string; + savedContent?: string; isLoading?: boolean; isEditable?: boolean; onContentChange?: (content: string) => void; @@ -70,6 +78,7 @@ function formatFileSize(bytes?: number): string { export function FileViewer({ file, content = '', + savedContent = '', isLoading = false, isEditable = false, onContentChange, @@ -77,9 +86,16 @@ export function FileViewer({ onDownload }: FileViewerProps) { const [editedContent, setEditedContent] = useState(content); + const [originalContent, setOriginalContent] = useState(savedContent || content); const [hasChanges, setHasChanges] = useState(false); const [showLargeFileWarning, setShowLargeFileWarning] = useState(false); const [forceShowAsText, setForceShowAsText] = useState(false); + const [showSearchPanel, setShowSearchPanel] = useState(false); + const [searchText, setSearchText] = useState(''); + const [replaceText, setReplaceText] = useState(''); + const [showReplacePanel, setShowReplacePanel] = useState(false); + const [searchMatches, setSearchMatches] = useState<{ start: number; end: number }[]>([]); + const [currentMatchIndex, setCurrentMatchIndex] = useState(-1); const fileTypeInfo = getFileType(file.name); @@ -100,7 +116,11 @@ export function FileViewer({ // 同步外部内容更改 useEffect(() => { setEditedContent(content); - setHasChanges(false); + // 只有在savedContent更新时才更新originalContent + if (savedContent) { + setOriginalContent(savedContent); + } + setHasChanges(content !== (savedContent || content)); // 如果是未知文件类型且文件较大,显示警告 if (fileTypeInfo.type === 'unknown' && isLargeFile && !forceShowAsText) { @@ -108,19 +128,137 @@ export function FileViewer({ } else { setShowLargeFileWarning(false); } - }, [content, fileTypeInfo.type, isLargeFile, forceShowAsText]); + }, [content, savedContent, fileTypeInfo.type, isLargeFile, forceShowAsText]); // 处理内容更改 const handleContentChange = (newContent: string) => { setEditedContent(newContent); - setHasChanges(newContent !== content); + setHasChanges(newContent !== originalContent); onContentChange?.(newContent); }; // 保存文件 const handleSave = () => { onSave?.(editedContent); + // 注意:不在这里更新originalContent,因为它会通过savedContent prop更新 + }; + + // 复原文件 + const handleRevert = () => { + setEditedContent(originalContent); setHasChanges(false); + onContentChange?.(originalContent); + }; + + // 搜索匹配功能 + const findMatches = (text: string) => { + if (!text) { + setSearchMatches([]); + setCurrentMatchIndex(-1); + return; + } + + const matches: { start: number; end: number }[] = []; + const regex = new RegExp(text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); + let match; + + while ((match = regex.exec(editedContent)) !== null) { + matches.push({ + start: match.index, + end: match.index + match[0].length + }); + // 避免无限循环 + if (match.index === regex.lastIndex) regex.lastIndex++; + } + + setSearchMatches(matches); + setCurrentMatchIndex(matches.length > 0 ? 0 : -1); + }; + + // 搜索导航 + const goToNextMatch = () => { + if (searchMatches.length === 0) return; + setCurrentMatchIndex((prev) => (prev + 1) % searchMatches.length); + }; + + const goToPrevMatch = () => { + if (searchMatches.length === 0) return; + setCurrentMatchIndex((prev) => (prev - 1 + searchMatches.length) % searchMatches.length); + }; + + // 替换功能 + const handleFindReplace = (findText: string, replaceWithText: string, replaceAll: boolean = false) => { + if (!findText) return; + + let newContent = editedContent; + if (replaceAll) { + newContent = newContent.replace(new RegExp(findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), replaceWithText); + } else if (currentMatchIndex >= 0 && searchMatches[currentMatchIndex]) { + // 替换当前匹配项 + const match = searchMatches[currentMatchIndex]; + newContent = editedContent.substring(0, match.start) + + replaceWithText + + editedContent.substring(match.end); + } + + setEditedContent(newContent); + setHasChanges(newContent !== originalContent); + onContentChange?.(newContent); + + // 重新搜索以更新匹配项 + setTimeout(() => findMatches(findText), 0); + }; + + const handleFind = () => { + setShowSearchPanel(true); + setShowReplacePanel(false); + }; + + const handleReplace = () => { + setShowSearchPanel(true); + setShowReplacePanel(true); + }; + + // 渲染带高亮的文本 + const renderHighlightedText = (text: string) => { + if (!searchText || searchMatches.length === 0) { + return text; + } + + const parts: React.ReactNode[] = []; + let lastIndex = 0; + + searchMatches.forEach((match, index) => { + // 添加匹配前的文本 + if (match.start > lastIndex) { + parts.push(text.substring(lastIndex, match.start)); + } + + // 添加高亮的匹配文本 + const isCurrentMatch = index === currentMatchIndex; + parts.push( + + {text.substring(match.start, match.end)} + + ); + + lastIndex = match.end; + }); + + // 添加最后的文本 + if (lastIndex < text.length) { + parts.push(text.substring(lastIndex)); + } + + return parts; }; // 处理用户确认打开大文件 @@ -167,16 +305,49 @@ export function FileViewer({