feat(ui): 添加编辑器和文件浏览器状态持久化

- FileExplorer: 保存展开的目录路径到 localStorage
- IDE: 保存打开的标签页和活动标签,刷新后自动恢复
- App: 调整 IDE 面板默认宽度为 70%
This commit is contained in:
2025-12-17 18:18:06 +08:00
parent c892069ea1
commit 4fc6b61134
3 changed files with 115 additions and 10 deletions
+100 -1
View File
@@ -4,13 +4,23 @@
* 整合文件浏览器和代码编辑器的 IDE 组件
*/
import { useState, useCallback, useEffect } from 'react';
import { useState, useCallback, useEffect, useRef } 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';
// localStorage 键名
const STORAGE_KEY_TABS = 'ai-assistant-editor-tabs';
const STORAGE_KEY_ACTIVE_TAB = 'ai-assistant-editor-active-tab';
// 保存的标签页格式(只保存路径,不保存内容)
interface SavedTab {
path: string;
name: string;
}
interface IDEProps {
className?: string;
/** 文件浏览器宽度 */
@@ -21,6 +31,8 @@ export function IDE({ className, sidebarWidth = 256 }: IDEProps) {
const [tabs, setTabs] = useState<EditorTab[]>([]);
const [activeTabId, setActiveTabId] = useState<string | null>(null);
const [workingDirectory, setWorkingDirectory] = useState<string>('');
const isRestoringTabs = useRef(false);
const hasRestoredTabs = useRef(false);
// 获取工作目录
useEffect(() => {
@@ -35,6 +47,93 @@ export function IDE({ className, sidebarWidth = 256 }: IDEProps) {
});
}, []);
// 从 localStorage 恢复标签页
useEffect(() => {
if (hasRestoredTabs.current) return;
hasRestoredTabs.current = true;
const restoreTabs = async () => {
try {
const savedTabsJson = localStorage.getItem(STORAGE_KEY_TABS);
const savedActivePath = localStorage.getItem(STORAGE_KEY_ACTIVE_TAB);
if (!savedTabsJson) return;
const savedTabs: SavedTab[] = JSON.parse(savedTabsJson);
if (savedTabs.length === 0) return;
isRestoringTabs.current = true;
// 逐个恢复标签页
const restoredTabs: EditorTab[] = [];
for (const savedTab of savedTabs) {
try {
const result = await readFile(savedTab.path);
if (result.success && result.data.encoding === 'utf-8') {
restoredTabs.push({
id: `tab-${Date.now()}-${restoredTabs.length}`,
path: savedTab.path,
name: savedTab.name,
content: result.data.content,
originalContent: result.data.content,
language: getLanguageFromFilename(savedTab.name),
});
}
} catch {
// 文件可能已删除,跳过
}
}
if (restoredTabs.length > 0) {
setTabs(restoredTabs);
// 恢复活动标签
if (savedActivePath) {
const activeTab = restoredTabs.find((tab) => tab.path === savedActivePath);
if (activeTab) {
setActiveTabId(activeTab.id);
} else {
setActiveTabId(restoredTabs[0].id);
}
} else {
setActiveTabId(restoredTabs[0].id);
}
}
isRestoringTabs.current = false;
} catch {
isRestoringTabs.current = false;
}
};
restoreTabs();
}, []);
// 保存标签页到 localStorage
useEffect(() => {
// 恢复过程中不保存
if (isRestoringTabs.current) return;
const tabsToSave: SavedTab[] = tabs.map((tab) => ({
path: tab.path,
name: tab.name,
}));
localStorage.setItem(STORAGE_KEY_TABS, JSON.stringify(tabsToSave));
}, [tabs]);
// 保存活动标签到 localStorage
useEffect(() => {
// 恢复过程中不保存
if (isRestoringTabs.current) return;
const activeTab = tabs.find((tab) => tab.id === activeTabId);
if (activeTab) {
localStorage.setItem(STORAGE_KEY_ACTIVE_TAB, activeTab.path);
} else {
localStorage.removeItem(STORAGE_KEY_ACTIVE_TAB);
}
}, [activeTabId, tabs]);
// 打开文件
const handleFileSelect = useCallback(async (path: string, name: string) => {
// 检查是否已打开