初始化提交

This commit is contained in:
2025-09-19 20:23:07 +08:00
parent c5ee5dd2a3
commit 7172b16917
36 changed files with 7302 additions and 100 deletions

View File

@@ -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

View File

@@ -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);
});
}
}
}
};

View File

@@ -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);

View File

@@ -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);
};
/**
* 清空所有记录
*/

View File

@@ -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, {

View File

@@ -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',