feat(ui): 添加 IDE 组件(文件浏览器 + 代码编辑器)
- 新增 CodeEditor 组件,基于 CodeMirror 实现多标签代码编辑 - 新增 FileExplorer 组件,支持文件树展开/折叠和文件选择 - 新增 IDE 组件,整合文件浏览器和代码编辑器 - 新增 SessionPanel 组件,用于会话管理 - 添加文件写入 API(PUT /api/files/write) - 优化布局:IDE 始终显示,移除文件切换按钮 - 工作目录路径显示在文件浏览器标题栏,支持悬浮显示完整路径
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* IDE Component
|
||||
*
|
||||
* 整合文件浏览器和代码编辑器的 IDE 组件
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '../utils/cn.js';
|
||||
import { readFile, getWorkingDirectory } from '../api/client.js';
|
||||
import { FileExplorer } from './FileExplorer.js';
|
||||
import { CodeEditor, getLanguageFromFilename, type EditorTab } from './CodeEditor.js';
|
||||
|
||||
interface IDEProps {
|
||||
className?: string;
|
||||
/** 文件浏览器宽度 */
|
||||
sidebarWidth?: number;
|
||||
}
|
||||
|
||||
export function IDE({ className, sidebarWidth = 256 }: IDEProps) {
|
||||
const [tabs, setTabs] = useState<EditorTab[]>([]);
|
||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||
const [workingDirectory, setWorkingDirectory] = useState<string>('');
|
||||
|
||||
// 获取工作目录
|
||||
useEffect(() => {
|
||||
getWorkingDirectory()
|
||||
.then((result) => {
|
||||
if (result?.data?.workingDirectory) {
|
||||
setWorkingDirectory(result.data.workingDirectory);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 忽略错误
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 打开文件
|
||||
const handleFileSelect = useCallback(async (path: string, name: string) => {
|
||||
// 检查是否已打开
|
||||
const existingTab = tabs.find((tab) => tab.path === path);
|
||||
if (existingTab) {
|
||||
setActiveTabId(existingTab.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
try {
|
||||
const result = await readFile(path);
|
||||
if (result.success && result.data.encoding === 'utf-8') {
|
||||
const newTab: EditorTab = {
|
||||
id: `tab-${Date.now()}`,
|
||||
path,
|
||||
name,
|
||||
content: result.data.content,
|
||||
originalContent: result.data.content,
|
||||
language: getLanguageFromFilename(name),
|
||||
};
|
||||
setTabs((prev) => [...prev, newTab]);
|
||||
setActiveTabId(newTab.id);
|
||||
} else if (result.data.encoding === 'base64') {
|
||||
toast.error('Cannot edit binary files');
|
||||
} else {
|
||||
toast.error('Failed to open file');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to open file:', error);
|
||||
toast.error('Failed to open file');
|
||||
}
|
||||
}, [tabs]);
|
||||
|
||||
// 切换标签
|
||||
const handleTabChange = useCallback((tabId: string) => {
|
||||
setActiveTabId(tabId);
|
||||
}, []);
|
||||
|
||||
// 关闭标签
|
||||
const handleTabClose = useCallback((tabId: string) => {
|
||||
const tabIndex = tabs.findIndex((tab) => tab.id === tabId);
|
||||
const tab = tabs[tabIndex];
|
||||
|
||||
// 检查是否有未保存的更改
|
||||
if (tab && tab.content !== tab.originalContent) {
|
||||
const confirmed = window.confirm(`${tab.name} has unsaved changes. Close anyway?`);
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
setTabs((prev) => prev.filter((tab) => tab.id !== tabId));
|
||||
|
||||
// 如果关闭的是当前标签,切换到相邻标签
|
||||
if (activeTabId === tabId) {
|
||||
if (tabs.length > 1) {
|
||||
const newIndex = tabIndex === 0 ? 1 : tabIndex - 1;
|
||||
setActiveTabId(tabs[newIndex].id);
|
||||
} else {
|
||||
setActiveTabId(null);
|
||||
}
|
||||
}
|
||||
}, [tabs, activeTabId]);
|
||||
|
||||
// 内容更改
|
||||
const handleContentChange = useCallback((tabId: string, content: string) => {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === tabId ? { ...tab, content } : tab
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 保存后更新原始内容
|
||||
const handleSave = useCallback((tabId: string, _path: string, content: string) => {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === tabId ? { ...tab, originalContent: content } : tab
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full', className)}>
|
||||
{/* 文件浏览器 */}
|
||||
<div
|
||||
className="flex-shrink-0 border-r border-line"
|
||||
style={{ width: sidebarWidth }}
|
||||
>
|
||||
<FileExplorer onFileSelect={handleFileSelect} workingDirectory={workingDirectory} />
|
||||
</div>
|
||||
|
||||
{/* 代码编辑器 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<CodeEditor
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
onTabChange={handleTabChange}
|
||||
onTabClose={handleTabClose}
|
||||
onContentChange={handleContentChange}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user