Files
Nano-Banana-AI-Image-Editor/src/App.tsx
2025-09-19 18:40:43 +08:00

164 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } 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, showHistory, setShowHistory } = useAppStore();
const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number} | null>(null);
const [previewPosition, setPreviewPosition] = useState<{x: number, y: number} | null>(null);
const [isPreviewVisible, setIsPreviewVisible] = useState(false);
// 在挂载时初始化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);
}, []);
// 控制预览窗口的显示和隐藏动画
useEffect(() => {
if (hoveredImage) {
// 延迟一小段时间后设置为可见,以触发动画
const timer = setTimeout(() => {
setIsPreviewVisible(true);
}, 10);
return () => clearTimeout(timer);
} else {
setIsPreviewVisible(false);
}
}, [hoveredImage]);
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 relative">
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out overflow-hidden", showPromptPanel ? "w-72" : "w-8")}>
<div className={cn("h-full", showPromptPanel ? "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={cn("flex-shrink-0 transition-all duration-300 ease-in-out overflow-hidden", showHistory ? "w-72" : "w-8")}>
<div className={cn("h-full", showHistory ? "card card-lg" : "")}>
<HistoryPanel setHoveredImage={setHoveredImage} setPreviewPosition={setPreviewPosition} />
</div>
</div>
</div>
{/* 悬浮预览 */}
{hoveredImage && (
<div
className="fixed inset-0 z-[99999] flex items-center justify-center pointer-events-none"
>
<div
className="bg-white rounded-xl shadow-2xl border border-gray-300 overflow-hidden max-w-2xl max-h-[80vh] flex flex-col transition-all duration-200 ease-out"
style={{
transform: isPreviewVisible ? 'scale(1)' : 'scale(0.8)',
opacity: isPreviewVisible ? 1 : 0,
transformOrigin: previewPosition ? `${previewPosition.x}px ${previewPosition.y}px` : 'center'
}}
>
<div className="bg-gray-900 text-white text-sm p-3 truncate font-medium">
{hoveredImage.title}
</div>
<div className="flex-1 flex items-center justify-center p-4">
<img
src={hoveredImage.url}
alt="预览"
className="max-w-full max-h-[60vh] object-contain"
/>
</div>
{/* 图像信息 */}
<div className="p-3 bg-gray-50 border-t border-gray-200 text-sm">
{hoveredImage.width && hoveredImage.height && (
<div className="flex justify-between text-gray-600">
<span>:</span>
<span className="text-gray-800 font-medium">{hoveredImage.width} × {hoveredImage.height}</span>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<ToastProvider>
<AppContent />
</ToastProvider>
</QueryClientProvider>
);
}
export default App;