diff --git a/IFLOW.md b/IFLOW.md
new file mode 100644
index 0000000..2b459ea
--- /dev/null
+++ b/IFLOW.md
@@ -0,0 +1,142 @@
+# Nano Banana AI Image Editor - iFlow 文档
+
+## 项目概述
+
+Nano Banana AI Image Editor 是一个基于 React 的 AI 图像编辑工具,用户可以通过直观的界面与 Google 的 Gemini AI 模型进行交互,实现图像生成和编辑功能。
+
+## 技术栈
+
+- **核心框架**: React 18.x (TypeScript)
+- **构建工具**: Vite 5.x
+- **语言**: TypeScript (ES2020)
+- **状态管理**: Zustand
+- **数据获取**: TanStack Query (React Query)
+- **UI 框架**:
+ - Tailwind CSS 3.x (样式框架)
+ - Radix UI (无样式的可访问 UI 组件)
+ - Lucide React (图标库)
+- **图像处理**:
+ - Konva (2D 画布库)
+ - react-konva (Konva 的 React 封装)
+- **本地存储**: IndexedDB (通过 idb-keyval 库操作)
+- **AI 集成**: Google Generative AI SDK (@google/genai)
+- **工具库**:
+ - class-variance-authority (组件变体管理)
+ - clsx + tailwind-merge (CSS 类名合并)
+ - fabric.js (备用画布库)
+
+## 代码风格和命名规范
+
+### 代码风格
+- 使用 TypeScript 严格模式 (strict: true)
+- 函数式组件为主,使用 React Hooks
+- 组件文件使用 .tsx 扩展名
+- 工具函数文件使用 .ts 扩展名
+- 使用 ESLint 进行代码检查
+- 启用严格的 TypeScript 编译选项
+
+### 命名规范
+- 组件文件和组件名使用 PascalCase (如 `Header.tsx`, `ImageCanvas`)
+- 工具函数和普通文件使用 camelCase (如 `imageUtils.ts`, `useAppStore.ts`)
+- 常量使用 UPPER_SNAKE_CASE
+- 变量和函数使用 camelCase
+- 组件 Props 接口命名为组件名 + Props (如 `ButtonProps`)
+- Hook 函数以 use 开头 (如 `useAppStore`, `useImageGeneration`)
+
+## 样式和 UI 框架
+
+### Tailwind CSS
+- 使用自定义颜色方案,以香蕉黄 (banana) 为主题色
+- 定义了 light 主题颜色 (背景、面板、边框、文字等)
+- 使用自定义间距、阴影和动画
+- 使用 tailwind-merge 和 clsx 进行类名合并
+
+### 组件设计
+- 使用 Radix UI 构建无样式的可访问组件
+- 自定义 UI 组件位于 `src/components/ui` 目录
+- 组件变体使用 class-variance-authority 管理
+- 图标使用 Lucide React
+
+## 项目结构
+
+```
+Nano-Banana-AI-Image-Editor/
+├── src/
+│ ├── components/ # React 组件
+│ │ ├── ui/ # 基础 UI 组件
+│ │ ├── Header.tsx # 应用头部
+│ │ ├── ImageCanvas.tsx # 图像画布
+│ │ ├── PromptComposer.tsx # 提示词编辑器
+│ │ ├── HistoryPanel.tsx # 历史记录面板
+│ │ └── ... # 其他组件
+│ ├── hooks/ # 自定义 React Hooks
+│ │ ├── useImageGeneration.ts # 图像生成 Hook
+│ │ ├── useIndexedDBListener.ts # IndexedDB 监听 Hook
+│ │ └── useKeyboardShortcuts.ts # 键盘快捷键 Hook
+│ ├── services/ # 业务逻辑服务
+│ │ ├── geminiService.ts # Gemini AI 服务
+│ │ ├── indexedDBService.ts # IndexedDB 操作服务
+│ │ ├── uploadService.ts # 文件上传服务
+│ │ └── cacheService.ts # 缓存服务
+│ ├── store/ # 状态管理
+│ │ └── useAppStore.ts # 全局状态管理 (Zustand)
+│ ├── utils/ # 工具函数
+│ │ ├── cn.ts # 类名合并工具
+│ │ └── imageUtils.ts # 图像处理工具
+│ ├── types/ # TypeScript 类型定义
+│ ├── App.tsx # 根组件
+│ ├── main.tsx # 应用入口
+│ └── vite-env.d.ts # Vite 类型声明
+├── public/ # 静态资源
+├── node_modules/ # 依赖包
+├── index.html # HTML 模板
+├── package.json # 项目配置
+├── tsconfig.json # TypeScript 配置
+├── vite.config.ts # Vite 配置
+├── tailwind.config.js # Tailwind 配置
+├── postcss.config.js # PostCSS 配置
+├── eslint.config.js # ESLint 配置
+└── README.md # 项目说明
+```
+
+## 核心功能模块
+
+### 1. 图像画布 (ImageCanvas)
+- 使用 Konva 和 react-konva 实现图像显示和编辑
+- 支持图像缩放、平移
+- 实现画笔工具进行遮罩绘制
+- 支持图像下载功能
+
+### 2. 提示词编辑 (PromptComposer)
+- 用户输入提示词生成图像
+- 提供提示词建议功能
+- 集成 AI 模型参数调整 (如风格、质量等)
+
+### 3. 历史记录 (HistoryPanel)
+- 显示生成的图像历史
+- 支持历史图像的查看和重新编辑
+- 使用 IndexedDB 存储历史数据
+
+### 4. 状态管理 (useAppStore)
+- 使用 Zustand 管理全局状态
+- 存储画布状态、用户设置、历史记录等
+- 提供状态操作方法
+
+### 5. AI 服务 (geminiService)
+- 集成 Google Gemini AI 模型
+- 实现图像生成和编辑功能
+- 处理与 AI 模型的交互
+
+## 开发环境配置
+
+1. 安装依赖: `npm install`
+2. 启动开发服务器: `npm run dev`
+3. 构建生产版本: `npm run build`
+4. 代码检查: `npm run lint`
+
+## 注意事项
+
+- 项目使用 IndexedDB 存储图像数据,需要注意存储空间管理
+- AI 功能需要配置 Google API 密钥
+- 图像处理功能依赖浏览器 Canvas API
+- 移动端适配需要特别关注界面布局和交互
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index e494eb8..143c1ab 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cn } from './utils/cn';
import { Header } from './components/Header';
@@ -23,6 +23,7 @@ function AppContent() {
useKeyboardShortcuts();
const { showPromptPanel, setShowPromptPanel, setShowHistory } = useAppStore();
+ const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number} | null>(null);
// 在挂载时初始化IndexedDB并清理base64数据
useEffect(() => {
@@ -78,7 +79,7 @@ function AppContent() {
-
+
@@ -91,10 +92,39 @@ function AppContent() {
+
+ {/* 悬浮预览 */}
+ {hoveredImage && (
+
+
+
+ {hoveredImage.title}
+
+
+

+
+ {/* 图像信息 */}
+
+ {hoveredImage.width && hoveredImage.height && (
+
+ 尺寸:
+ {hoveredImage.width} × {hoveredImage.height}
+
+ )}
+
+
+
+ )}
);
}
diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx
index 37ef7bd..8e460a4 100644
--- a/src/components/HistoryPanel.tsx
+++ b/src/components/HistoryPanel.tsx
@@ -9,7 +9,7 @@ import { useIndexedDBListener } from '../hooks/useIndexedDBListener';
import { DayPicker } from 'react-day-picker';
import zhCN from 'react-day-picker/dist/locale/zh-CN';
-export const HistoryPanel: React.FC = () => {
+export const HistoryPanel: React.FC<{ setHoveredImage: (image: {url: string, title: string, width?: number, height?: number} | null) => void }> = ({ setHoveredImage }) => {
const {
currentProject,
canvasImage,
@@ -63,9 +63,7 @@ export const HistoryPanel: React.FC = () => {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20; // 减少每页显示的项目数
- // 悬浮预览状态
- const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number} | null>(null);
- const [previewPosition, setPreviewPosition] = useState<{x: number, y: number}>({x: 0, y: 0});
+
const generations = currentProject?.generations || [];
const edits = currentProject?.edits || [];
@@ -536,49 +534,6 @@ export const HistoryPanel: React.FC = () => {
width: img.width,
height: img.height
});
-
- // 计算预览位置,确保不超出屏幕边界
- const previewWidth = 300; // 减小预览窗口大小
- const previewHeight = 300;
- const offsetX = 10;
- const offsetY = 10;
-
-
- // 获取HistoryPanel的位置
- const historyPanel = document.querySelector('.w-72.bg-white.p-4');
- const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 };
-
- // 计算相对于HistoryPanel的位置
- let x = e.clientX - panelRect.left + offsetX;
- let y = e.clientY - panelRect.top + offsetY;
-
-
-
- // 确保预览窗口不会超出右边界
- if (x + previewWidth > window.innerWidth) {
- x = window.innerWidth - previewWidth - 10;
- }
-
- // 确保预览窗口不会超出下边界
- if (y + previewHeight > window.innerHeight) {
- y = window.innerHeight - previewHeight - 10;
- }
-
- // 确保预览窗口不会超出左边界
- if (x < 0) {
- x = 10;
- }
-
- // 确保预览窗口不会超出上边界
- if (y < 0) {
- y = 10;
- }
-
- // 添加额外的安全边界检查
- x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
- y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
-
- setPreviewPosition({x, y});
};
img.onerror = (error) => {
console.error('图像加载失败:', error);
@@ -589,89 +544,12 @@ export const HistoryPanel: React.FC = () => {
width: 0,
height: 0
});
-
- // 计算预览位置
- const previewWidth = 300;
- const previewHeight = 300;
- const offsetX = 10;
- const offsetY = 10;
-
-
- let x = e.clientX + offsetX;
- let y = e.clientY + offsetY;
-
- // 确保预览窗口不会超出右边界
- if (x + previewWidth > window.innerWidth) {
- x = window.innerWidth - previewWidth - 10;
- }
-
- // 确保预览窗口不会超出下边界
- if (y + previewHeight > window.innerHeight) {
- y = window.innerHeight - previewHeight - 10;
- }
-
- // 确保预览窗口不会超出左边界
- if (x < 0) {
- x = 10;
- }
-
- // 确保预览窗口不会超出上边界
- if (y < 0) {
- y = 10;
- }
-
- // 添加额外的安全边界检查
- x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
- y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
-
- setPreviewPosition({x, y});
};
img.src = imageUrl;
}
}}
- onMouseMove={(e) => {
- // 调整预览位置以避免被遮挡
- const previewWidth = 300;
- const previewHeight = 300;
- const offsetX = 10;
- const offsetY = 10;
-
-
- // 获取HistoryPanel的位置
- const historyPanel = document.querySelector('.w-72.bg-white.p-4');
- const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 };
-
- // 计算相对于HistoryPanel的位置
- let x = e.clientX - panelRect.left + offsetX;
- let y = e.clientY - panelRect.top + offsetY;
-
- // 确保预览窗口不会超出右边界
- if (x + previewWidth > (historyPanel ? historyPanel.clientWidth : window.innerWidth)) {
- x = (historyPanel ? historyPanel.clientWidth : window.innerWidth) - previewWidth - 10;
- }
-
- // 确保预览窗口不会超出下边界
- if (y + previewHeight > (historyPanel ? historyPanel.clientHeight : window.innerHeight)) {
- y = (historyPanel ? historyPanel.clientHeight : window.innerHeight) - previewHeight - 10;
- }
-
- // 确保预览窗口不会超出左边界
- if (x < 0) {
- x = 10;
- }
-
- // 确保预览窗口不会超出上边界
- if (y < 0) {
- y = 10;
- }
-
- // 添加额外的安全边界检查
- const maxWidth = historyPanel ? historyPanel.clientWidth : window.innerWidth;
- const maxHeight = historyPanel ? historyPanel.clientHeight : window.innerHeight;
- x = Math.max(10, Math.min(x, maxWidth - previewWidth - 10));
- y = Math.max(10, Math.min(y, maxHeight - previewHeight - 10));
-
- setPreviewPosition({x, y});
+ onMouseMove={() => {
+ // 不需要处理鼠标移动事件,因为预览现在在页面中心显示
}}
onMouseLeave={() => {
setHoveredImage(null);
@@ -753,42 +631,6 @@ export const HistoryPanel: React.FC = () => {
width: img.width,
height: img.height
});
-
- // 计算预览位置,确保不超出屏幕边界
- const previewWidth = 300;
- const previewHeight = 300;
- const offsetX = 10;
- const offsetY = 10;
-
-
- let x = e.clientX + offsetX;
- let y = e.clientY + offsetY;
-
- // 确保预览窗口不会超出右边界
- if (x + previewWidth > window.innerWidth) {
- x = window.innerWidth - previewWidth - 10;
- }
-
- // 确保预览窗口不会超出下边界
- if (y + previewHeight > window.innerHeight) {
- y = window.innerHeight - previewHeight - 10;
- }
-
- // 确保预览窗口不会超出左边界
- if (x < 0) {
- x = 10;
- }
-
- // 确保预览窗口不会超出上边界
- if (y < 0) {
- y = 10;
- }
-
- // 添加额外的安全边界检查
- x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
- y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
-
- setPreviewPosition({x, y});
};
img.onerror = (error) => {
console.error('图像加载失败:', error);
@@ -799,85 +641,12 @@ export const HistoryPanel: React.FC = () => {
width: 0,
height: 0
});
-
- // 计算预览位置
- const previewWidth = 300;
- const previewHeight = 300;
- const offsetX = 10;
- const offsetY = 10;
-
- // 获取HistoryPanel的位置信息
- const historyPanel = e.currentTarget.closest('.w-72');
- const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 };
-
- // 计算相对于整个视窗的位置
- let x = e.clientX + offsetX;
- let y = e.clientY + offsetY;
-
- // 确保预览窗口不会超出右边界
- if (x + previewWidth > window.innerWidth) {
- x = window.innerWidth - previewWidth - 10;
- }
-
- // 确保预览窗口不会超出下边界
- if (y + previewHeight > window.innerHeight) {
- y = window.innerHeight - previewHeight - 10;
- }
-
- // 确保预览窗口不会超出左边界
- if (x < 0) {
- x = 10;
- }
-
- // 确保预览窗口不会超出上边界
- if (y < 0) {
- y = 10;
- }
-
- // 添加额外的安全边界检查
- x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
- y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
-
- setPreviewPosition({x, y});
};
img.src = imageUrl;
}
}}
- onMouseMove={(e) => {
- // 调整预览位置以避免被遮挡
- const previewWidth = 300;
- const previewHeight = 300;
- const offsetX = 10;
- const offsetY = 10;
-
- let x = e.clientX + offsetX;
- let y = e.clientY + offsetY;
-
- // 确保预览窗口不会超出右边界
- if (x + previewWidth > window.innerWidth) {
- x = window.innerWidth - previewWidth - 10;
- }
-
- // 确保预览窗口不会超出下边界
- if (y + previewHeight > window.innerHeight) {
- y = window.innerHeight - previewHeight - 10;
- }
-
- // 确保预览窗口不会超出左边界
- if (x < 0) {
- x = 10;
- }
-
- // 确保预览窗口不会超出上边界
- if (y < 0) {
- y = 10;
- }
-
- // 添加额外的安全边界检查
- x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
- y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
-
- setPreviewPosition({x, y});
+ onMouseMove={() => {
+ // 不需要处理鼠标移动事件,因为预览现在在页面中心显示
}}
onMouseLeave={() => {
setHoveredImage(null);
@@ -1188,37 +957,6 @@ export const HistoryPanel: React.FC = () => {
title={previewModal.title}
description={previewModal.description}
/>
-
- {/* 悬浮预览 */}
- {hoveredImage && (
-
-
- {hoveredImage.title}
-
-

- {/* 图像信息 */}
-
- {hoveredImage.width && hoveredImage.height && (
-
- 尺寸:
- {hoveredImage.width} × {hoveredImage.height}
-
- )}
-
-
- )}
);
};
\ No newline at end of file
diff --git a/src/services/uploadService.ts b/src/services/uploadService.ts
index abe1f8c..3e973cf 100644
--- a/src/services/uploadService.ts
+++ b/src/services/uploadService.ts
@@ -2,31 +2,31 @@
import { UploadResult } from '../types'
// 上传接口URL
-const UPLOAD_URL = 'https://api.pandorastudio.cn/auth/OSSupload'
+const UPLOAD_URL = import.meta.env.VITE_UPLOAD_API
// 创建一个Map来缓存已上传的图像
const uploadCache = new Map