You've already forked Nano-Banana-AI-Image-Editor
添加了IFLOW描述文件;
优化 调整了历史记录悬浮窗的显示位置;
This commit is contained in:
142
IFLOW.md
Normal file
142
IFLOW.md
Normal file
@@ -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
|
||||||
|
- 移动端适配需要特别关注界面布局和交互
|
||||||
36
src/App.tsx
36
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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { cn } from './utils/cn';
|
import { cn } from './utils/cn';
|
||||||
import { Header } from './components/Header';
|
import { Header } from './components/Header';
|
||||||
@@ -23,6 +23,7 @@ function AppContent() {
|
|||||||
useKeyboardShortcuts();
|
useKeyboardShortcuts();
|
||||||
|
|
||||||
const { showPromptPanel, setShowPromptPanel, setShowHistory } = useAppStore();
|
const { showPromptPanel, setShowPromptPanel, setShowHistory } = useAppStore();
|
||||||
|
const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number} | null>(null);
|
||||||
|
|
||||||
// 在挂载时初始化IndexedDB并清理base64数据
|
// 在挂载时初始化IndexedDB并清理base64数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -78,7 +79,7 @@ function AppContent() {
|
|||||||
<Header />
|
<Header />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex overflow-hidden p-4 gap-4">
|
<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", !showPromptPanel && "w-8")}>
|
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out", !showPromptPanel && "w-8")}>
|
||||||
<div className="h-full card card-lg">
|
<div className="h-full card card-lg">
|
||||||
<PromptComposer />
|
<PromptComposer />
|
||||||
@@ -91,10 +92,39 @@ function AppContent() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="h-full card card-lg">
|
<div className="h-full card card-lg">
|
||||||
<HistoryPanel />
|
<HistoryPanel setHoveredImage={setHoveredImage} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useIndexedDBListener } from '../hooks/useIndexedDBListener';
|
|||||||
import { DayPicker } from 'react-day-picker';
|
import { DayPicker } from 'react-day-picker';
|
||||||
import zhCN from 'react-day-picker/dist/locale/zh-CN';
|
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 {
|
const {
|
||||||
currentProject,
|
currentProject,
|
||||||
canvasImage,
|
canvasImage,
|
||||||
@@ -63,9 +63,7 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const itemsPerPage = 20; // 减少每页显示的项目数
|
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 generations = currentProject?.generations || [];
|
||||||
const edits = currentProject?.edits || [];
|
const edits = currentProject?.edits || [];
|
||||||
@@ -536,49 +534,6 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
width: img.width,
|
width: img.width,
|
||||||
height: img.height
|
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) => {
|
img.onerror = (error) => {
|
||||||
console.error('图像加载失败:', error);
|
console.error('图像加载失败:', error);
|
||||||
@@ -589,89 +544,12 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
width: 0,
|
width: 0,
|
||||||
height: 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;
|
img.src = imageUrl;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseMove={(e) => {
|
onMouseMove={() => {
|
||||||
// 调整预览位置以避免被遮挡
|
// 不需要处理鼠标移动事件,因为预览现在在页面中心显示
|
||||||
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});
|
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setHoveredImage(null);
|
setHoveredImage(null);
|
||||||
@@ -753,42 +631,6 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
width: img.width,
|
width: img.width,
|
||||||
height: img.height
|
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) => {
|
img.onerror = (error) => {
|
||||||
console.error('图像加载失败:', error);
|
console.error('图像加载失败:', error);
|
||||||
@@ -799,85 +641,12 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
width: 0,
|
width: 0,
|
||||||
height: 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;
|
img.src = imageUrl;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseMove={(e) => {
|
onMouseMove={() => {
|
||||||
// 调整预览位置以避免被遮挡
|
// 不需要处理鼠标移动事件,因为预览现在在页面中心显示
|
||||||
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});
|
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onMouseLeave={() => {
|
||||||
setHoveredImage(null);
|
setHoveredImage(null);
|
||||||
@@ -1188,37 +957,6 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
title={previewModal.title}
|
title={previewModal.title}
|
||||||
description={previewModal.description}
|
description={previewModal.description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 悬浮预览 */}
|
|
||||||
{hoveredImage && (
|
|
||||||
<div
|
|
||||||
className="absolute z-[9999] shadow-2xl border border-gray-300 rounded-lg overflow-hidden bg-white backdrop-blur-sm pointer-events-none"
|
|
||||||
style={{
|
|
||||||
left: `${previewPosition.x}px`,
|
|
||||||
top: `${previewPosition.y}px`,
|
|
||||||
maxWidth: '200px', // 减小最大宽度
|
|
||||||
maxHeight: '200px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="bg-gray-900 text-white text-xs p-2 truncate font-medium">
|
|
||||||
{hoveredImage.title}
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
src={hoveredImage.url}
|
|
||||||
alt="预览"
|
|
||||||
className="w-full h-auto max-h-[150px] object-contain"
|
|
||||||
/>
|
|
||||||
{/* 图像信息 */}
|
|
||||||
<div className="p-2 bg-gray-50 border-t border-gray-200 text-xs">
|
|
||||||
{hoveredImage.width && hoveredImage.height && (
|
|
||||||
<div className="flex justify-between text-gray-600">
|
|
||||||
<span>尺寸:</span>
|
|
||||||
<span className="text-gray-800">{hoveredImage.width} × {hoveredImage.height}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -2,31 +2,31 @@
|
|||||||
import { UploadResult } from '../types'
|
import { UploadResult } from '../types'
|
||||||
|
|
||||||
// 上传接口URL
|
// 上传接口URL
|
||||||
const UPLOAD_URL = 'https://api.pandorastudio.cn/auth/OSSupload'
|
const UPLOAD_URL = import.meta.env.VITE_UPLOAD_API
|
||||||
|
|
||||||
// 创建一个Map来缓存已上传的图像
|
// 创建一个Map来缓存已上传的图像
|
||||||
const uploadCache = new Map<string, UploadResult>()
|
const uploadCache = new Map<string, UploadResult>()
|
||||||
|
|
||||||
// 缓存配置
|
// 缓存配置
|
||||||
const MAX_CACHE_SIZE = 50; // 减少最大缓存条目数
|
const MAX_CACHE_SIZE = 50 // 减少最大缓存条目数
|
||||||
const CACHE_EXPIRY_TIME = 15 * 60 * 1000; // 缓存过期时间15分钟
|
const CACHE_EXPIRY_TIME = 15 * 60 * 1000 // 缓存过期时间15分钟
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理过期的缓存条目
|
* 清理过期的缓存条目
|
||||||
*/
|
*/
|
||||||
function cleanupExpiredCache(): void {
|
function cleanupExpiredCache(): void {
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
let deletedCount = 0;
|
let deletedCount = 0
|
||||||
|
|
||||||
uploadCache.forEach((value, key) => {
|
uploadCache.forEach((value, key) => {
|
||||||
if (now - value.timestamp > CACHE_EXPIRY_TIME) {
|
if (now - value.timestamp > CACHE_EXPIRY_TIME) {
|
||||||
uploadCache.delete(key);
|
uploadCache.delete(key)
|
||||||
deletedCount++;
|
deletedCount++
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (deletedCount > 0) {
|
if (deletedCount > 0) {
|
||||||
console.log(`清除了 ${deletedCount} 个过期的缓存条目`);
|
console.log(`清除了 ${deletedCount} 个过期的缓存条目`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,16 +37,16 @@ function maintainCacheSize(): void {
|
|||||||
// 如果缓存大小超过限制,删除最旧的条目
|
// 如果缓存大小超过限制,删除最旧的条目
|
||||||
if (uploadCache.size >= MAX_CACHE_SIZE) {
|
if (uploadCache.size >= MAX_CACHE_SIZE) {
|
||||||
// 获取所有条目并按时间排序
|
// 获取所有条目并按时间排序
|
||||||
const entries = Array.from(uploadCache.entries());
|
const entries = Array.from(uploadCache.entries())
|
||||||
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
entries.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||||
|
|
||||||
// 删除最旧的条目,直到缓存大小在限制内
|
// 删除最旧的条目,直到缓存大小在限制内
|
||||||
const deleteCount = Math.max(1, Math.floor(MAX_CACHE_SIZE * 0.2)); // 删除20%的条目
|
const deleteCount = Math.max(1, Math.floor(MAX_CACHE_SIZE * 0.2)) // 删除20%的条目
|
||||||
for (let i = 0; i < deleteCount && uploadCache.size >= MAX_CACHE_SIZE; i++) {
|
for (let i = 0; i < deleteCount && uploadCache.size >= MAX_CACHE_SIZE; i++) {
|
||||||
uploadCache.delete(entries[i][0]);
|
uploadCache.delete(entries[i][0])
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`缓存已满,删除了 ${deleteCount} 个最旧的条目`);
|
console.log(`缓存已满,删除了 ${deleteCount} 个最旧的条目`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,17 +60,17 @@ function getImageHash(imageData: string): string {
|
|||||||
if (imageData.startsWith('blob:')) {
|
if (imageData.startsWith('blob:')) {
|
||||||
// 对于Blob URL,我们使用URL本身作为标识符的一部分
|
// 对于Blob URL,我们使用URL本身作为标识符的一部分
|
||||||
// 这不是完美的解决方案,但对于大多数情况足够了
|
// 这不是完美的解决方案,但对于大多数情况足够了
|
||||||
return btoa(imageData).slice(0, 32);
|
return btoa(imageData).slice(0, 32)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于base64数据,使用简单的哈希函数生成图像标识符
|
// 对于base64数据,使用简单的哈希函数生成图像标识符
|
||||||
let hash = 0;
|
let hash = 0
|
||||||
for (let i = 0; i < imageData.length; i++) {
|
for (let i = 0; i < imageData.length; i++) {
|
||||||
const char = imageData.charCodeAt(i);
|
const char = imageData.charCodeAt(i)
|
||||||
hash = ((hash << 5) - hash) + char;
|
hash = (hash << 5) - hash + char
|
||||||
hash = hash & hash; // 转换为32位整数
|
hash = hash & hash // 转换为32位整数
|
||||||
}
|
}
|
||||||
return hash.toString();
|
return hash.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,37 +81,37 @@ function getImageHash(imageData: string): string {
|
|||||||
async function getBlobFromUrl(blobUrl: string): Promise<Blob> {
|
async function getBlobFromUrl(blobUrl: string): Promise<Blob> {
|
||||||
try {
|
try {
|
||||||
// 从AppStore获取Blob
|
// 从AppStore获取Blob
|
||||||
const { useAppStore } = await import('../store/useAppStore');
|
const { useAppStore } = await import('../store/useAppStore')
|
||||||
const blob = useAppStore.getState().getBlob(blobUrl);
|
const blob = useAppStore.getState().getBlob(blobUrl)
|
||||||
|
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
// 如果AppStore中没有找到Blob,尝试从URL获取
|
// 如果AppStore中没有找到Blob,尝试从URL获取
|
||||||
console.warn('无法从AppStore获取Blob,尝试从URL获取:', blobUrl);
|
console.warn('无法从AppStore获取Blob,尝试从URL获取:', blobUrl)
|
||||||
try {
|
try {
|
||||||
const response = await fetch(blobUrl);
|
const response = await fetch(blobUrl)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`获取Blob失败: ${response.status} ${response.statusText}`);
|
throw new Error(`获取Blob失败: ${response.status} ${response.statusText}`)
|
||||||
}
|
}
|
||||||
return await response.blob();
|
return await response.blob()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('从URL获取Blob失败:', error);
|
console.error('从URL获取Blob失败:', error)
|
||||||
throw new Error('无法从Blob URL获取图像数据');
|
throw new Error('无法从Blob URL获取图像数据')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return blob;
|
return blob
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('从AppStore获取Blob时出错:', error);
|
console.error('从AppStore获取Blob时出错:', error)
|
||||||
// 如果导入AppStore失败,直接尝试从URL获取
|
// 如果导入AppStore失败,直接尝试从URL获取
|
||||||
try {
|
try {
|
||||||
const response = await fetch(blobUrl);
|
const response = await fetch(blobUrl)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`获取Blob失败: ${response.status} ${response.statusText}`);
|
throw new Error(`获取Blob失败: ${response.status} ${response.statusText}`)
|
||||||
}
|
}
|
||||||
return await response.blob();
|
return await response.blob()
|
||||||
} catch (fetchError) {
|
} catch (fetchError) {
|
||||||
console.error('从URL获取Blob失败:', fetchError);
|
console.error('从URL获取Blob失败:', fetchError)
|
||||||
throw new Error('无法从Blob URL获取图像数据');
|
throw new Error('无法从Blob URL获取图像数据')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,118 +125,118 @@ async function getBlobFromUrl(blobUrl: string): Promise<Blob> {
|
|||||||
*/
|
*/
|
||||||
export const uploadImage = async (imageData: string | Blob, accessToken: string, skipCache: boolean = false): Promise<{ success: boolean; url?: string; error?: string }> => {
|
export const uploadImage = async (imageData: string | Blob, accessToken: string, skipCache: boolean = false): Promise<{ success: boolean; url?: string; error?: string }> => {
|
||||||
// 检查缓存中是否已有该图像的上传结果
|
// 检查缓存中是否已有该图像的上传结果
|
||||||
const imageHash = typeof imageData === 'string' ? getImageHash(imageData) : 'blob-' + Date.now();
|
const imageHash = typeof imageData === 'string' ? getImageHash(imageData) : 'blob-' + Date.now()
|
||||||
|
|
||||||
if (!skipCache && typeof imageData === 'string' && uploadCache.has(imageHash)) {
|
if (!skipCache && typeof imageData === 'string' && uploadCache.has(imageHash)) {
|
||||||
const cachedResult = uploadCache.get(imageHash)!;
|
const cachedResult = uploadCache.get(imageHash)!
|
||||||
// 检查缓存是否过期
|
// 检查缓存是否过期
|
||||||
if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) {
|
if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) {
|
||||||
console.log('从缓存中获取上传结果');
|
console.log('从缓存中获取上传结果')
|
||||||
return cachedResult;
|
return cachedResult
|
||||||
} else {
|
} else {
|
||||||
// 缓存过期,删除它
|
// 缓存过期,删除它
|
||||||
uploadCache.delete(imageHash);
|
uploadCache.delete(imageHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let blob: Blob;
|
let blob: Blob
|
||||||
|
|
||||||
if (typeof imageData === 'string') {
|
if (typeof imageData === 'string') {
|
||||||
if (imageData.startsWith('blob:')) {
|
if (imageData.startsWith('blob:')) {
|
||||||
// 从Blob URL获取Blob数据
|
// 从Blob URL获取Blob数据
|
||||||
blob = await getBlobFromUrl(imageData);
|
blob = await getBlobFromUrl(imageData)
|
||||||
} else if (imageData.includes('base64,')) {
|
} else if (imageData.includes('base64,')) {
|
||||||
// 从base64数据创建Blob
|
// 从base64数据创建Blob
|
||||||
const base64Data = imageData.split('base64,')[1];
|
const base64Data = imageData.split('base64,')[1]
|
||||||
const byteString = atob(base64Data);
|
const byteString = atob(base64Data)
|
||||||
const mimeString = 'image/png'; // 默认MIME类型
|
const mimeString = 'image/png' // 默认MIME类型
|
||||||
const ab = new ArrayBuffer(byteString.length);
|
const ab = new ArrayBuffer(byteString.length)
|
||||||
const ia = new Uint8Array(ab);
|
const ia = new Uint8Array(ab)
|
||||||
for (let i = 0; i < byteString.length; i++) {
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
ia[i] = byteString.charCodeAt(i);
|
ia[i] = byteString.charCodeAt(i)
|
||||||
}
|
}
|
||||||
blob = new Blob([ab], { type: mimeString });
|
blob = new Blob([ab], { type: mimeString })
|
||||||
} else {
|
} else {
|
||||||
// 从URL获取Blob
|
// 从URL获取Blob
|
||||||
const response = await fetch(imageData);
|
const response = await fetch(imageData)
|
||||||
blob = await response.blob();
|
blob = await response.blob()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果已经是Blob对象,直接使用
|
// 如果已经是Blob对象,直接使用
|
||||||
blob = imageData;
|
blob = imageData
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建FormData对象
|
// 创建FormData对象
|
||||||
const formData = new FormData();
|
const formData = new FormData()
|
||||||
formData.append('file', blob, 'generated-image.png');
|
formData.append('file', blob, 'generated-image.png')
|
||||||
|
|
||||||
// 发送POST请求
|
// 发送POST请求
|
||||||
const response = await fetch(UPLOAD_URL, {
|
const response = await fetch(UPLOAD_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'accessToken': accessToken,
|
accessToken: accessToken,
|
||||||
// 添加其他可能需要的头部
|
// 添加其他可能需要的头部
|
||||||
},
|
},
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
})
|
||||||
|
|
||||||
// 记录响应状态以帮助调试
|
// 记录响应状态以帮助调试
|
||||||
console.log('上传响应状态:', response.status, response.statusText);
|
console.log('上传响应状态:', response.status, response.statusText)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text()
|
||||||
console.error('上传失败响应内容:', errorText);
|
console.error('上传失败响应内容:', errorText)
|
||||||
throw new Error(`上传失败: ${response.status} ${response.statusText} - ${errorText}`);
|
throw new Error(`上传失败: ${response.status} ${response.statusText} - ${errorText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json()
|
||||||
console.log('上传响应结果:', result);
|
console.log('上传响应结果:', result)
|
||||||
|
|
||||||
// 根据返回格式处理结果: {"code": 200,"msg": "上传成功","data": "9ecbaa0a0.jpg"}
|
// 根据返回格式处理结果: {"code": 200,"msg": "上传成功","data": "9ecbaa0a0.jpg"}
|
||||||
if (result.code === 200) {
|
if (result.code === 200) {
|
||||||
// 使用环境变量中的VITE_UPLOAD_ASSET_URL作为前缀
|
// 使用环境变量中的VITE_UPLOAD_ASSET_URL作为前缀
|
||||||
const uploadAssetUrl = import.meta.env.VITE_UPLOAD_ASSET_URL || '';
|
const uploadAssetUrl = import.meta.env.VITE_UPLOAD_ASSET_URL || ''
|
||||||
const fullUrl = uploadAssetUrl ? `${uploadAssetUrl}/${result.data}` : result.data;
|
const fullUrl = uploadAssetUrl ? `${uploadAssetUrl}/${result.data}` : result.data
|
||||||
|
|
||||||
// 清理过期缓存
|
// 清理过期缓存
|
||||||
cleanupExpiredCache();
|
cleanupExpiredCache()
|
||||||
|
|
||||||
// 维护缓存大小
|
// 维护缓存大小
|
||||||
maintainCacheSize();
|
maintainCacheSize()
|
||||||
|
|
||||||
// 将上传结果存储到缓存中
|
// 将上传结果存储到缓存中
|
||||||
const uploadResult = { success: true, url: fullUrl, error: undefined };
|
const uploadResult = { success: true, url: fullUrl, error: undefined }
|
||||||
if (typeof imageData === 'string') {
|
if (typeof imageData === 'string') {
|
||||||
uploadCache.set(imageHash, {
|
uploadCache.set(imageHash, {
|
||||||
...uploadResult,
|
...uploadResult,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return uploadResult;
|
return uploadResult
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`上传失败: ${result.msg}`);
|
throw new Error(`上传失败: ${result.msg}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('上传图像时出错:', error);
|
console.error('上传图像时出错:', error)
|
||||||
const errorResult = { success: false, error: error instanceof Error ? error.message : String(error) };
|
const errorResult = { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||||
|
|
||||||
// 清理过期缓存
|
// 清理过期缓存
|
||||||
cleanupExpiredCache();
|
cleanupExpiredCache()
|
||||||
|
|
||||||
// 维护缓存大小(即使是失败的结果也缓存,但时间较短)
|
// 维护缓存大小(即使是失败的结果也缓存,但时间较短)
|
||||||
maintainCacheSize();
|
maintainCacheSize()
|
||||||
|
|
||||||
// 将失败的上传结果也存储到缓存中(可选)
|
// 将失败的上传结果也存储到缓存中(可选)
|
||||||
if (typeof imageData === 'string') {
|
if (typeof imageData === 'string') {
|
||||||
uploadCache.set(imageHash, {
|
uploadCache.set(imageHash, {
|
||||||
...errorResult,
|
...errorResult,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now(),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorResult;
|
return errorResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,43 +249,43 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
|||||||
*/
|
*/
|
||||||
export const uploadImages = async (imageDatas: (string | Blob)[], accessToken: string, skipCache: boolean = false): Promise<UploadResult[]> => {
|
export const uploadImages = async (imageDatas: (string | Blob)[], accessToken: string, skipCache: boolean = false): Promise<UploadResult[]> => {
|
||||||
try {
|
try {
|
||||||
const results: UploadResult[] = [];
|
const results: UploadResult[] = []
|
||||||
|
|
||||||
for (let i = 0; i < imageDatas.length; i++) {
|
for (let i = 0; i < imageDatas.length; i++) {
|
||||||
const imageData = imageDatas[i];
|
const imageData = imageDatas[i]
|
||||||
try {
|
try {
|
||||||
const uploadResult = await uploadImage(imageData, accessToken, skipCache);
|
const uploadResult = await uploadImage(imageData, accessToken, skipCache)
|
||||||
const result: UploadResult = {
|
const result: UploadResult = {
|
||||||
success: uploadResult.success,
|
success: uploadResult.success,
|
||||||
url: uploadResult.url,
|
url: uploadResult.url,
|
||||||
error: uploadResult.error,
|
error: uploadResult.error,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
}
|
||||||
results.push(result);
|
results.push(result)
|
||||||
console.log(`第${i + 1}张图像上传${uploadResult.success ? '成功' : '失败'}:`, uploadResult);
|
console.log(`第${i + 1}张图像上传${uploadResult.success ? '成功' : '失败'}:`, uploadResult)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const result: UploadResult = {
|
const result: UploadResult = {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
}
|
||||||
results.push(result);
|
results.push(result)
|
||||||
console.error(`第${i + 1}张图像上传失败:`, error);
|
console.error(`第${i + 1}张图像上传失败:`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有任何上传失败
|
// 检查是否有任何上传失败
|
||||||
const failedUploads = results.filter(r => !r.success);
|
const failedUploads = results.filter(r => !r.success)
|
||||||
if (failedUploads.length > 0) {
|
if (failedUploads.length > 0) {
|
||||||
console.warn(`${failedUploads.length}张图像上传失败`);
|
console.warn(`${failedUploads.length}张图像上传失败`)
|
||||||
} else {
|
} else {
|
||||||
console.log(`所有${results.length}张图像上传成功`);
|
console.log(`所有${results.length}张图像上传成功`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量上传图像时出错:', error);
|
console.error('批量上传图像时出错:', error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +293,6 @@ export const uploadImages = async (imageDatas: (string | Blob)[], accessToken: s
|
|||||||
* 清除上传缓存
|
* 清除上传缓存
|
||||||
*/
|
*/
|
||||||
export const clearUploadCache = (): void => {
|
export const clearUploadCache = (): void => {
|
||||||
uploadCache.clear();
|
uploadCache.clear()
|
||||||
console.log('上传缓存已清除');
|
console.log('上传缓存已清除')
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user