/** * FileBrowser Component * * 文件浏览器组件 */ import { useState, useEffect, useCallback } from 'react'; import { listFiles, readFile, type FileInfo } from '../api/client.js'; interface FileBrowserProps { onFileSelect?: (path: string, content: string) => void; className?: string; } // 文件图标 const FileIcon = ({ type, extension }: { type: 'file' | 'directory'; extension?: string }) => { if (type === 'directory') { return ( ); } // 根据扩展名显示不同颜色 const colors: Record = { ts: 'text-blue-400', tsx: 'text-blue-400', js: 'text-yellow-300', jsx: 'text-yellow-300', json: 'text-yellow-500', md: 'text-fg-muted', css: 'text-pink-400', html: 'text-orange-400', py: 'text-green-400', go: 'text-cyan-400', rs: 'text-orange-500', }; const color = colors[extension || ''] || 'text-fg-muted'; return ( ); }; // 格式化文件大小 const formatSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; }; export function FileBrowser({ onFileSelect, className = '' }: FileBrowserProps) { const [currentPath, setCurrentPath] = useState('.'); const [files, setFiles] = useState([]); const [parentPath, setParentPath] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [fileContent, setFileContent] = useState(null); const [showHidden, setShowHidden] = useState(false); // 加载目录内容 const loadDirectory = useCallback(async (path: string) => { setLoading(true); setError(null); setSelectedFile(null); setFileContent(null); try { const response = await listFiles(path, showHidden); setFiles(response.data.files); setCurrentPath(response.data.path); setParentPath(response.data.parent); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load directory'); } finally { setLoading(false); } }, [showHidden]); // 初始加载 useEffect(() => { loadDirectory('.'); }, [loadDirectory]); // 处理文件/目录点击 const handleItemClick = async (item: FileInfo) => { if (item.type === 'directory') { loadDirectory(item.path); } else { setSelectedFile(item.path); try { const response = await readFile(item.path); if (response.data.encoding === 'utf-8') { setFileContent(response.data.content); onFileSelect?.(item.path, response.data.content); } else { setFileContent('[Binary file]'); } } catch (err) { setFileContent(`Error: ${err instanceof Error ? err.message : 'Failed to read file'}`); } } }; // 返回上级目录 const handleGoUp = () => { if (parentPath !== null) { loadDirectory(parentPath); } }; // 刷新 const handleRefresh = () => { loadDirectory(currentPath); }; return (
{/* 工具栏 */}
{currentPath}
{/* 文件列表 */}
{loading ? (
Loading...
) : error ? (
{error}
) : files.length === 0 ? (
Empty directory
) : (
{files.map((file) => (
handleItemClick(file)} className={`flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-surface-subtle ${ selectedFile === file.path ? 'bg-surface-subtle border-l-2 border-blue-500' : '' }`} > {file.name} {file.type === 'file' && ( {formatSize(file.size)} )}
))}
)}
{/* 文件预览 */} {selectedFile && fileContent && (
{selectedFile}
            {fileContent.slice(0, 5000)}
            {fileContent.length > 5000 && '\n... (truncated)'}
          
)}
); }