You've already forked Nano-Banana-AI-Image-Editor
初始化提交
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { Button } from './ui/Button';
|
||||
import { History, Download, Image as ImageIcon } from 'lucide-react';
|
||||
import { History, Download, Image as ImageIcon, Trash2 } from 'lucide-react';
|
||||
import { cn } from '../utils/cn';
|
||||
import { ImagePreviewModal } from './ImagePreviewModal';
|
||||
import * as indexedDBService from '../services/indexedDBService';
|
||||
@@ -20,7 +20,11 @@ export const HistoryPanel: React.FC = () => {
|
||||
showHistory,
|
||||
setShowHistory,
|
||||
setCanvasImage,
|
||||
selectedTool
|
||||
selectedTool,
|
||||
deleteGeneration,
|
||||
deleteEdit,
|
||||
deleteGenerations,
|
||||
deleteEdits
|
||||
} = useAppStore();
|
||||
|
||||
const { getBlob } = useAppStore.getState();
|
||||
@@ -37,6 +41,19 @@ export const HistoryPanel: React.FC = () => {
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 删除确认对话框状态
|
||||
const [deleteConfirm, setDeleteConfirm] = React.useState<{
|
||||
open: boolean;
|
||||
ids: string[];
|
||||
type: 'generation' | 'edit' | 'multiple';
|
||||
count: number;
|
||||
}>({
|
||||
open: false,
|
||||
ids: [],
|
||||
type: 'generation',
|
||||
count: 0
|
||||
});
|
||||
|
||||
// 存储从Blob URL解码的图像数据
|
||||
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -479,9 +496,32 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="mb-6 flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">变体</h4>
|
||||
<span className="text-xs text-gray-400">
|
||||
{filteredGenerations.length + filteredEdits.length}/100
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-gray-400">
|
||||
{filteredGenerations.length + filteredEdits.length}/100
|
||||
</span>
|
||||
{(filteredGenerations.length > 0 || filteredEdits.length > 0) && (
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700 text-xs flex items-center"
|
||||
onClick={() => {
|
||||
const allIds = [
|
||||
...filteredGenerations.map(g => g.id),
|
||||
...filteredEdits.map(e => e.id)
|
||||
];
|
||||
setDeleteConfirm({
|
||||
open: true,
|
||||
ids: allIds,
|
||||
type: 'multiple',
|
||||
count: allIds.length
|
||||
});
|
||||
}}
|
||||
title="清空所有历史记录"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
清空
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{filteredGenerations.length === 0 && filteredEdits.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
@@ -697,6 +737,23 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="absolute top-1 left-1 bg-gray-900/80 text-xs px-1 py-0.5 rounded text-white">
|
||||
G{globalIndex + 1}
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<button
|
||||
className="absolute top-1 right-1 bg-red-500/80 text-white p-1 rounded-full hover:bg-red-600 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteConfirm({
|
||||
open: true,
|
||||
ids: [generation.id],
|
||||
type: 'generation',
|
||||
count: 1
|
||||
});
|
||||
}}
|
||||
title="删除记录"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -903,6 +960,23 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="absolute top-1 left-1 bg-purple-900/80 text-xs px-1 py-0.5 rounded text-white">
|
||||
E{globalIndex + 1}
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<button
|
||||
className="absolute top-1 right-1 bg-red-500/80 text-white p-1 rounded-full hover:bg-red-600 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteConfirm({
|
||||
open: true,
|
||||
ids: [edit.id],
|
||||
type: 'edit',
|
||||
count: 1
|
||||
});
|
||||
}}
|
||||
title="删除记录"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1010,6 +1084,53 @@ export const HistoryPanel: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 生成结果图像 */}
|
||||
{gen.outputAssets && gen.outputAssets.length > 0 && (
|
||||
<div className="pt-2 border-t border-gray-200">
|
||||
<h5 className="text-xs font-medium text-gray-500 mb-2">生成结果</h5>
|
||||
<div className="text-xs text-gray-600 mb-2">
|
||||
{gen.outputAssets.length} 个生成结果
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{gen.outputAssets.slice(0, 4).map((asset: any, index: number) => {
|
||||
// 获取上传后的远程链接(如果存在)
|
||||
const uploadedUrl = gen.uploadResults && gen.uploadResults[index] && gen.uploadResults[index].success
|
||||
? `${gen.uploadResults[index].url}?x-oss-process=image/quality,q_30`
|
||||
: null;
|
||||
|
||||
const displayUrl = uploadedUrl || asset.url;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={asset.id}
|
||||
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer hover:ring-2 hover:ring-yellow-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPreviewModal({
|
||||
open: true,
|
||||
imageUrl: displayUrl,
|
||||
title: `生成结果 ${index + 1}`,
|
||||
description: `${asset.width} × ${asset.height}`
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={displayUrl}
|
||||
alt={`生成结果 ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{gen.outputAssets.length > 4 && (
|
||||
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
|
||||
+{gen.outputAssets.length - 4}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 参考图像信息 */}
|
||||
{gen.sourceAssets && gen.sourceAssets.length > 0 && (
|
||||
<div className="pt-2 border-t border-gray-200">
|
||||
@@ -1020,10 +1141,15 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => {
|
||||
// 获取上传后的远程链接(如果存在)
|
||||
// 参考图像在uploadResults中从索引1开始(索引0是生成的图像)
|
||||
const uploadedUrl = gen.uploadResults && gen.uploadResults[index + 1] && gen.uploadResults[index + 1].success
|
||||
? `${gen.uploadResults[index + 1].url}?x-oss-process=image/quality,q_30`
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
// 但由于gen可能是轻量级记录,我们需要从dbGenerations中获取完整的记录
|
||||
const fullGen = dbGenerations.find(g => g.id === gen.id) || gen;
|
||||
const outputAssetsCount = fullGen.outputAssets?.length || 0;
|
||||
|
||||
const uploadedUrl = gen.uploadResults && gen.uploadResults[outputAssetsCount + index] && gen.uploadResults[outputAssetsCount + index].success
|
||||
? `${gen.uploadResults[outputAssetsCount + index].url}?x-oss-process=image/quality,q_30`
|
||||
: null;
|
||||
|
||||
const displayUrl = uploadedUrl || asset.url;
|
||||
|
||||
return (
|
||||
@@ -1128,9 +1254,10 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => {
|
||||
// 获取上传后的远程链接(如果存在)
|
||||
// 参考图像在uploadResults中从索引1开始(索引0是生成的图像)
|
||||
const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[index + 1] && parentGen.uploadResults[index + 1].success
|
||||
? `${parentGen.uploadResults[index + 1].url}?x-oss-process=image/quality,q_30`
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = parentGen.outputAssets?.length || 0;
|
||||
const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[outputAssetsCount + index] && parentGen.uploadResults[outputAssetsCount + index].success
|
||||
? `${parentGen.uploadResults[outputAssetsCount + index].url}?x-oss-process=image/quality,q_30`
|
||||
: null;
|
||||
const displayUrl = uploadedUrl || asset.url;
|
||||
|
||||
@@ -1189,6 +1316,68 @@ export const HistoryPanel: React.FC = () => {
|
||||
description={previewModal.description}
|
||||
/>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
{deleteConfirm.open && (
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center z-[100] backdrop-blur-sm rounded-lg">
|
||||
<div className="bg-white rounded-xl p-6 card-lg max-w-xs w-full mx-4">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
|
||||
<Trash2 className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">确认删除</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
{deleteConfirm.count > 1
|
||||
? `确定要删除这 ${deleteConfirm.count} 条历史记录吗?此操作无法撤销。`
|
||||
: '确定要删除这条历史记录吗?此操作无法撤销。'}
|
||||
</p>
|
||||
<div className="flex justify-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs px-4 py-2 h-8 border-gray-200 text-gray-600 hover:bg-gray-100 card"
|
||||
onClick={() => setDeleteConfirm(prev => ({ ...prev, open: false }))}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="text-xs px-4 py-2 h-8 card"
|
||||
onClick={() => {
|
||||
// 执行删除操作
|
||||
if (deleteConfirm.type === 'generation') {
|
||||
deleteConfirm.ids.forEach(id => deleteGeneration(id));
|
||||
} else if (deleteConfirm.type === 'edit') {
|
||||
deleteConfirm.ids.forEach(id => deleteEdit(id));
|
||||
} else {
|
||||
// 多选删除
|
||||
const genIds = deleteConfirm.ids.filter(id =>
|
||||
filteredGenerations.some(g => g.id === id)
|
||||
);
|
||||
const editIds = deleteConfirm.ids.filter(id =>
|
||||
filteredEdits.some(e => e.id === id)
|
||||
);
|
||||
|
||||
if (genIds.length > 0) {
|
||||
deleteGenerations(genIds);
|
||||
}
|
||||
if (editIds.length > 0) {
|
||||
deleteEdits(editIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
setDeleteConfirm(prev => ({ ...prev, open: false }));
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 悬浮预览 */}
|
||||
{hoveredImage && (
|
||||
<div
|
||||
|
||||
@@ -318,7 +318,103 @@ export const ImageCanvas: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
// 直接下载当前画布内容
|
||||
// 首先尝试从当前选中的生成记录或编辑记录中获取上传后的URL
|
||||
const { selectedGenerationId, selectedEditId, currentProject } = useAppStore.getState();
|
||||
|
||||
// 获取当前选中的记录
|
||||
let selectedRecord = null;
|
||||
if (selectedGenerationId && currentProject) {
|
||||
selectedRecord = currentProject.generations.find(g => g.id === selectedGenerationId);
|
||||
} else if (selectedEditId && currentProject) {
|
||||
selectedRecord = currentProject.edits.find(e => e.id === selectedEditId);
|
||||
}
|
||||
|
||||
// 如果有选中的记录且有上传结果,尝试下载上传后的图像
|
||||
if (selectedRecord && selectedRecord.uploadResults && selectedRecord.uploadResults.length > 0) {
|
||||
// 下载第一个上传结果(通常是生成的图像)
|
||||
const uploadResult = selectedRecord.uploadResults[0];
|
||||
if (uploadResult.success && uploadResult.url) {
|
||||
// 使用async IIFE处理异步操作
|
||||
(async () => {
|
||||
try {
|
||||
// 首先尝试使用fetch获取图像数据
|
||||
const response = await fetch(uploadResult.url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
|
||||
// 创建下载链接
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
console.log('上传后的图像下载成功:', uploadResult.url);
|
||||
} catch (error) {
|
||||
console.error('使用fetch下载上传后的图像时出错:', error);
|
||||
// 如果fetch失败(可能是跨域问题),使用Canvas方案
|
||||
try {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous'; // 设置跨域属性
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 创建canvas并绘制图像
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 将canvas转换为blob并下载
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
console.log('使用Canvas方案下载成功');
|
||||
} else {
|
||||
console.error('Canvas转换为blob失败');
|
||||
}
|
||||
}, 'image/png');
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas处理失败:', canvasError);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = (imgError) => {
|
||||
console.error('图像加载失败:', imgError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
};
|
||||
|
||||
img.src = uploadResult.url;
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas方案也失败了:', canvasError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// 立即返回,让异步操作在后台进行
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有上传后的URL或下载失败,回退到下载当前画布内容
|
||||
const stage = stageRef.current;
|
||||
if (stage) {
|
||||
try {
|
||||
@@ -350,9 +446,14 @@ export const ImageCanvas: React.FC = () => {
|
||||
document.body.removeChild(link);
|
||||
} else if (canvasImage.startsWith('blob:')) {
|
||||
// Blob URL格式
|
||||
fetch(canvasImage)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
// 使用async IIFE处理异步操作
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(canvasImage);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -362,15 +463,66 @@ export const ImageCanvas: React.FC = () => {
|
||||
document.body.removeChild(link);
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
})
|
||||
.catch(error => {
|
||||
} catch (error) {
|
||||
console.error('下载Blob图像时出错:', error);
|
||||
});
|
||||
// 如果fetch失败(可能是跨域问题),使用Canvas方案
|
||||
try {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 创建canvas并绘制图像
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 将canvas转换为blob并下载
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
console.log('使用Canvas方案下载成功');
|
||||
} else {
|
||||
console.error('Canvas转换为blob失败');
|
||||
}
|
||||
}, 'image/png');
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas处理失败:', canvasError);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = (imgError) => {
|
||||
console.error('图像加载失败:', imgError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
};
|
||||
|
||||
img.src = canvasImage;
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas方案也失败了:', canvasError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
}
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
// 普通URL格式
|
||||
fetch(canvasImage)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
// 使用async IIFE处理异步操作
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(canvasImage);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -380,81 +532,60 @@ export const ImageCanvas: React.FC = () => {
|
||||
document.body.removeChild(link);
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
})
|
||||
.catch(error => {
|
||||
} catch (error) {
|
||||
console.error('下载图像时出错:', error);
|
||||
// 如果fetch失败,尝试直接下载
|
||||
const link = document.createElement('a');
|
||||
link.href = canvasImage;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
// 如果fetch失败(可能是跨域问题),使用Canvas方案
|
||||
try {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous'; // 设置跨域属性
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 创建canvas并绘制图像
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 将canvas转换为blob并下载
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
console.log('使用Canvas方案下载成功');
|
||||
} else {
|
||||
console.error('Canvas转换为blob失败');
|
||||
}
|
||||
}, 'image/png');
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas处理失败:', canvasError);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = (imgError) => {
|
||||
console.error('图像加载失败:', imgError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
};
|
||||
|
||||
img.src = canvasImage;
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas方案也失败了:', canvasError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('Stage未初始化,无法下载画布内容');
|
||||
|
||||
// 回退到下载原始图像
|
||||
if (canvasImage) {
|
||||
// 处理不同类型的URL
|
||||
if (canvasImage.startsWith('data:')) {
|
||||
// base64格式
|
||||
const link = document.createElement('a');
|
||||
link.href = canvasImage;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else if (canvasImage.startsWith('blob:')) {
|
||||
// Blob URL格式
|
||||
fetch(canvasImage)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('下载Blob图像时出错:', error);
|
||||
});
|
||||
} else {
|
||||
// 普通URL格式
|
||||
fetch(canvasImage)
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('下载图像时出错:', error);
|
||||
// 如果fetch失败,尝试直接下载
|
||||
const link = document.createElement('a');
|
||||
link.href = canvasImage;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -107,11 +107,21 @@ export const useImageGeneration = () => {
|
||||
// 上传参考图像(如果存在,使用缓存机制)
|
||||
let referenceUploadResults: any[] = [];
|
||||
if (request.referenceImages && request.referenceImages.length > 0) {
|
||||
// 将参考图像也转换为Blob URL
|
||||
const referenceUrls = await Promise.all(request.referenceImages.map(async (blob) => {
|
||||
return useAppStore.getState().addBlob(blob);
|
||||
// 将参考图像转换为base64字符串格式上传(与老版本保持一致)
|
||||
const referenceBase64s = await Promise.all(request.referenceImages.map(async (blob) => {
|
||||
if (typeof blob === 'string') {
|
||||
// 如果已经是base64字符串,直接返回
|
||||
return blob;
|
||||
} else {
|
||||
// 如果是Blob对象,转换为base64字符串
|
||||
return new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
}));
|
||||
referenceUploadResults = await uploadImages(referenceUrls, accessToken, false);
|
||||
referenceUploadResults = await uploadImages(referenceBase64s, accessToken, false);
|
||||
}
|
||||
|
||||
// 合并上传结果
|
||||
@@ -186,6 +196,10 @@ export const useImageGeneration = () => {
|
||||
|
||||
addGeneration(generation);
|
||||
setCanvasImage(outputAssets[0].url);
|
||||
|
||||
// 自动选择新生成的记录
|
||||
const { selectGeneration } = useAppStore.getState();
|
||||
selectGeneration(generation.id);
|
||||
}
|
||||
setIsGenerating(false);
|
||||
},
|
||||
@@ -482,7 +496,24 @@ export const useImageEditing = () => {
|
||||
try {
|
||||
const imageUrls = outputAssets.map(asset => asset.url);
|
||||
// 上传编辑后的图像(跳过缓存,因为这些是新生成的图像)
|
||||
uploadResults = await uploadImages(imageUrls, accessToken, true);
|
||||
const outputUploadResults = await uploadImages(imageUrls, accessToken, true);
|
||||
|
||||
// 上传参考图像(如果存在,使用缓存机制)
|
||||
let referenceUploadResults: any[] = [];
|
||||
if (referenceImageBlobs.length > 0) {
|
||||
// 将参考图像转换为base64字符串格式上传(与老版本保持一致)
|
||||
const referenceBase64s = await Promise.all(referenceImageBlobs.map(async (blob) => {
|
||||
return new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}));
|
||||
referenceUploadResults = await uploadImages(referenceBase64s, accessToken, false);
|
||||
}
|
||||
|
||||
// 合并上传结果
|
||||
uploadResults = [...outputUploadResults, ...referenceUploadResults];
|
||||
|
||||
// 检查上传结果
|
||||
const failedUploads = uploadResults.filter(r => !r.success);
|
||||
|
||||
@@ -410,6 +410,74 @@ export const cleanupBase64Data = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除指定的生成记录
|
||||
*/
|
||||
export const deleteGeneration = async (id: string): Promise<void> => {
|
||||
const db = getDB();
|
||||
const transaction = db.transaction([GENERATIONS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(GENERATIONS_STORE);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除指定的编辑记录
|
||||
*/
|
||||
export const deleteEdit = async (id: string): Promise<void> => {
|
||||
const db = getDB();
|
||||
const transaction = db.transaction([EDITS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(EDITS_STORE);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除生成记录
|
||||
*/
|
||||
export const deleteGenerations = async (ids: string[]): Promise<void> => {
|
||||
const db = getDB();
|
||||
const transaction = db.transaction([GENERATIONS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(GENERATIONS_STORE);
|
||||
|
||||
const promises = ids.map(id => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除编辑记录
|
||||
*/
|
||||
export const deleteEdits = async (ids: string[]): Promise<void> => {
|
||||
const db = getDB();
|
||||
const transaction = db.transaction([EDITS_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(EDITS_STORE);
|
||||
|
||||
const promises = ids.map(id => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空所有记录
|
||||
*/
|
||||
|
||||
@@ -167,9 +167,12 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
||||
blob = imageData;
|
||||
}
|
||||
|
||||
// 创建FormData对象
|
||||
// 创建FormData对象,使用唯一文件名
|
||||
const timestamp = Date.now();
|
||||
const random = Math.floor(Math.random() * 10000);
|
||||
const fileName = `image-${timestamp}-${random}.png`;
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, 'generated-image.png');
|
||||
formData.append('file', blob, fileName);
|
||||
|
||||
// 发送POST请求
|
||||
const response = await fetch(UPLOAD_URL, {
|
||||
|
||||
@@ -119,6 +119,12 @@ interface AppState {
|
||||
|
||||
setSelectedTool: (tool: 'generate' | 'edit' | 'mask') => void;
|
||||
|
||||
// 删除历史记录
|
||||
deleteGeneration: (id: string) => void;
|
||||
deleteEdit: (id: string) => void;
|
||||
deleteGenerations: (ids: string[]) => void;
|
||||
deleteEdits: (ids: string[]) => void;
|
||||
|
||||
// Blob存储操作
|
||||
addBlob: (blob: Blob) => string;
|
||||
getBlob: (url: string) => Blob | undefined;
|
||||
@@ -643,7 +649,243 @@ export const useAppStore = create<AppState>()(
|
||||
set({ blobStore: newBlobStore });
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 删除单个生成记录
|
||||
deleteGeneration: (id) => set((state) => {
|
||||
if (!state.currentProject) return {};
|
||||
|
||||
// 找到要删除的记录
|
||||
const generationToDelete = state.currentProject.generations.find(gen => gen.id === id);
|
||||
if (!generationToDelete) return {};
|
||||
|
||||
// 收集需要释放的Blob URLs
|
||||
const urlsToRevoke: string[] = [];
|
||||
|
||||
// 收集生成记录中的Blob URLs
|
||||
generationToDelete.sourceAssets.forEach(asset => {
|
||||
if (asset.blobUrl.startsWith('blob:')) {
|
||||
urlsToRevoke.push(asset.blobUrl);
|
||||
}
|
||||
});
|
||||
generationToDelete.outputAssetsBlobUrls.forEach(url => {
|
||||
if (url.startsWith('blob:')) {
|
||||
urlsToRevoke.push(url);
|
||||
}
|
||||
});
|
||||
|
||||
// 从IndexedDB中删除记录
|
||||
indexedDBService.deleteGeneration(id).catch(err => {
|
||||
console.error('从IndexedDB删除生成记录失败:', err);
|
||||
});
|
||||
|
||||
// 释放Blob URLs
|
||||
if (urlsToRevoke.length > 0) {
|
||||
set((innerState) => {
|
||||
urlsToRevoke.forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
const newBlobStore = new Map(innerState.blobStore);
|
||||
newBlobStore.delete(url);
|
||||
innerState = { ...innerState, blobStore: newBlobStore };
|
||||
});
|
||||
return innerState;
|
||||
});
|
||||
}
|
||||
|
||||
// 如果删除的是当前选中的记录,清除选择
|
||||
let selectedGenerationId = state.selectedGenerationId;
|
||||
if (selectedGenerationId === id) {
|
||||
selectedGenerationId = null;
|
||||
}
|
||||
|
||||
// 更新项目中的生成记录列表
|
||||
const updatedGenerations = state.currentProject.generations.filter(gen => gen.id !== id);
|
||||
|
||||
return {
|
||||
currentProject: {
|
||||
...state.currentProject,
|
||||
generations: updatedGenerations,
|
||||
updatedAt: Date.now()
|
||||
},
|
||||
selectedGenerationId
|
||||
};
|
||||
}),
|
||||
|
||||
// 删除单个编辑记录
|
||||
deleteEdit: (id) => set((state) => {
|
||||
if (!state.currentProject) return {};
|
||||
|
||||
// 找到要删除的记录
|
||||
const editToDelete = state.currentProject.edits.find(edit => edit.id === id);
|
||||
if (!editToDelete) return {};
|
||||
|
||||
// 收集需要释放的Blob URLs
|
||||
const urlsToRevoke: string[] = [];
|
||||
|
||||
// 收集编辑记录中的Blob URLs
|
||||
if (editToDelete.maskReferenceAssetBlobUrl && editToDelete.maskReferenceAssetBlobUrl.startsWith('blob:')) {
|
||||
urlsToRevoke.push(editToDelete.maskReferenceAssetBlobUrl);
|
||||
}
|
||||
editToDelete.outputAssetsBlobUrls.forEach(url => {
|
||||
if (url.startsWith('blob:')) {
|
||||
urlsToRevoke.push(url);
|
||||
}
|
||||
});
|
||||
|
||||
// 从IndexedDB中删除记录
|
||||
indexedDBService.deleteEdit(id).catch(err => {
|
||||
console.error('从IndexedDB删除编辑记录失败:', err);
|
||||
});
|
||||
|
||||
// 释放Blob URLs
|
||||
if (urlsToRevoke.length > 0) {
|
||||
set((innerState) => {
|
||||
urlsToRevoke.forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
const newBlobStore = new Map(innerState.blobStore);
|
||||
newBlobStore.delete(url);
|
||||
innerState = { ...innerState, blobStore: newBlobStore };
|
||||
});
|
||||
return innerState;
|
||||
});
|
||||
}
|
||||
|
||||
// 如果删除的是当前选中的记录,清除选择
|
||||
let selectedEditId = state.selectedEditId;
|
||||
if (selectedEditId === id) {
|
||||
selectedEditId = null;
|
||||
}
|
||||
|
||||
// 更新项目中的编辑记录列表
|
||||
const updatedEdits = state.currentProject.edits.filter(edit => edit.id !== id);
|
||||
|
||||
return {
|
||||
currentProject: {
|
||||
...state.currentProject,
|
||||
edits: updatedEdits,
|
||||
updatedAt: Date.now()
|
||||
},
|
||||
selectedEditId
|
||||
};
|
||||
}),
|
||||
|
||||
// 批量删除生成记录
|
||||
deleteGenerations: (ids) => set((state) => {
|
||||
if (!state.currentProject) return {};
|
||||
|
||||
// 收集需要释放的Blob URLs
|
||||
const urlsToRevoke: string[] = [];
|
||||
|
||||
// 收集所有要删除记录中的Blob URLs
|
||||
state.currentProject.generations.forEach(gen => {
|
||||
if (ids.includes(gen.id)) {
|
||||
gen.sourceAssets.forEach(asset => {
|
||||
if (asset.blobUrl.startsWith('blob:')) {
|
||||
urlsToRevoke.push(asset.blobUrl);
|
||||
}
|
||||
});
|
||||
gen.outputAssetsBlobUrls.forEach(url => {
|
||||
if (url.startsWith('blob:')) {
|
||||
urlsToRevoke.push(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 从IndexedDB中批量删除记录
|
||||
indexedDBService.deleteGenerations(ids).catch(err => {
|
||||
console.error('从IndexedDB批量删除生成记录失败:', err);
|
||||
});
|
||||
|
||||
// 释放Blob URLs
|
||||
if (urlsToRevoke.length > 0) {
|
||||
set((innerState) => {
|
||||
urlsToRevoke.forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
const newBlobStore = new Map(innerState.blobStore);
|
||||
newBlobStore.delete(url);
|
||||
innerState = { ...innerState, blobStore: newBlobStore };
|
||||
});
|
||||
return innerState;
|
||||
});
|
||||
}
|
||||
|
||||
// 如果删除的是当前选中的记录,清除选择
|
||||
let selectedGenerationId = state.selectedGenerationId;
|
||||
if (selectedGenerationId && ids.includes(selectedGenerationId)) {
|
||||
selectedGenerationId = null;
|
||||
}
|
||||
|
||||
// 更新项目中的生成记录列表
|
||||
const updatedGenerations = state.currentProject.generations.filter(gen => !ids.includes(gen.id));
|
||||
|
||||
return {
|
||||
currentProject: {
|
||||
...state.currentProject,
|
||||
generations: updatedGenerations,
|
||||
updatedAt: Date.now()
|
||||
},
|
||||
selectedGenerationId
|
||||
};
|
||||
}),
|
||||
|
||||
// 批量删除编辑记录
|
||||
deleteEdits: (ids) => set((state) => {
|
||||
if (!state.currentProject) return {};
|
||||
|
||||
// 收集需要释放的Blob URLs
|
||||
const urlsToRevoke: string[] = [];
|
||||
|
||||
// 收集所有要删除记录中的Blob URLs
|
||||
state.currentProject.edits.forEach(edit => {
|
||||
if (ids.includes(edit.id)) {
|
||||
if (edit.maskReferenceAssetBlobUrl && edit.maskReferenceAssetBlobUrl.startsWith('blob:')) {
|
||||
urlsToRevoke.push(edit.maskReferenceAssetBlobUrl);
|
||||
}
|
||||
edit.outputAssetsBlobUrls.forEach(url => {
|
||||
if (url.startsWith('blob:')) {
|
||||
urlsToRevoke.push(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 从IndexedDB中批量删除记录
|
||||
indexedDBService.deleteEdits(ids).catch(err => {
|
||||
console.error('从IndexedDB批量删除编辑记录失败:', err);
|
||||
});
|
||||
|
||||
// 释放Blob URLs
|
||||
if (urlsToRevoke.length > 0) {
|
||||
set((innerState) => {
|
||||
urlsToRevoke.forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
const newBlobStore = new Map(innerState.blobStore);
|
||||
newBlobStore.delete(url);
|
||||
innerState = { ...innerState, blobStore: newBlobStore };
|
||||
});
|
||||
return innerState;
|
||||
});
|
||||
}
|
||||
|
||||
// 如果删除的是当前选中的记录,清除选择
|
||||
let selectedEditId = state.selectedEditId;
|
||||
if (selectedEditId && ids.includes(selectedEditId)) {
|
||||
selectedEditId = null;
|
||||
}
|
||||
|
||||
// 更新项目中的编辑记录列表
|
||||
const updatedEdits = state.currentProject.edits.filter(edit => !ids.includes(edit.id));
|
||||
|
||||
return {
|
||||
currentProject: {
|
||||
...state.currentProject,
|
||||
edits: updatedEdits,
|
||||
updatedAt: Date.now()
|
||||
},
|
||||
selectedEditId
|
||||
};
|
||||
})
|
||||
}),
|
||||
{
|
||||
name: 'nano-banana-store',
|
||||
|
||||
Reference in New Issue
Block a user