Files
Nano-Banana-AI-Image-Editor/src/App.tsx
2025-09-19 01:25:30 +08:00

112 lines
3.1 KiB
TypeScript

import React, { useEffect } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cn } from './utils/cn';
import { Header } from './components/Header';
import { PromptComposer } from './components/PromptComposer';
import { ImageCanvas } from './components/ImageCanvas';
import { HistoryPanel } from './components/HistoryPanel';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
import { useAppStore } from './store/useAppStore';
import { ToastProvider } from './components/ToastContext';
import * as indexedDBService from './services/indexedDBService';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5分钟
retry: 2,
},
},
});
function AppContent() {
useKeyboardShortcuts();
const { showPromptPanel, setShowPromptPanel, setShowHistory } = useAppStore();
// 在挂载时初始化IndexedDB并清理base64数据
useEffect(() => {
const init = async () => {
try {
await indexedDBService.initDB();
// 清理已有的base64数据
await indexedDBService.cleanupBase64Data();
} catch (err) {
console.error('初始化IndexedDB或清理base64数据失败:', err);
}
};
init();
}, []);
// 在挂载时设置移动设备默认值
React.useEffect(() => {
const checkMobile = () => {
const isMobile = window.innerWidth < 768;
if (isMobile) {
setShowPromptPanel(false);
setShowHistory(false);
}
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, [setShowPromptPanel, setShowHistory]);
// 定期清理旧的历史记录
useEffect(() => {
const interval = setInterval(() => {
useAppStore.getState().cleanupOldHistory();
}, 30000); // 每30秒清理一次
return () => clearInterval(interval);
}, []);
// 定期清理未使用的Blob URL
useEffect(() => {
const interval = setInterval(() => {
useAppStore.getState().scheduleBlobCleanup();
}, 60000); // 每分钟清理一次
return () => clearInterval(interval);
}, []);
return (
<div className="h-screen bg-gray-50 text-gray-900 flex flex-col font-sans">
<div className="card card-lg rounded-none">
<Header />
</div>
<div className="flex-1 flex overflow-hidden p-4 gap-4">
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out", !showPromptPanel && "w-8")}>
<div className="h-full card card-lg">
<PromptComposer />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="h-full card card-lg">
<ImageCanvas />
</div>
</div>
<div className="flex-shrink-0">
<div className="h-full card card-lg">
<HistoryPanel />
</div>
</div>
</div>
</div>
);
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<ToastProvider>
<AppContent />
</ToastProvider>
</QueryClientProvider>
);
}
export default App;