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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user