修复内存溢出问题

This commit is contained in:
2025-09-19 01:25:30 +08:00
parent 803cc100be
commit 9674740c0d
13 changed files with 1085 additions and 337 deletions

View File

@@ -61,10 +61,10 @@ export const HistoryPanel: React.FC = () => {
// 分页状态
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 30; // 每页显示的项目数
const itemsPerPage = 20; // 减少每页显示的项目数
// 悬浮预览状态
const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number, size?: number} | null>(null);
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 || [];
@@ -76,7 +76,7 @@ export const HistoryPanel: React.FC = () => {
const uploadResult = generationOrEdit.uploadResults[index];
if (uploadResult.success && uploadResult.url) {
// 添加参数以降低图片质量
return `${uploadResult.url}?x-oss-process=image/quality,q_50`;
return `${uploadResult.url}?x-oss-process=image/quality,q_30`; // 降低质量到30%
}
}
return null;
@@ -480,7 +480,7 @@ export const HistoryPanel: React.FC = () => {
<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}/1000
{filteredGenerations.length + filteredEdits.length}/100
</span>
</div>
{filteredGenerations.length === 0 && filteredEdits.length === 0 ? (
@@ -530,25 +530,16 @@ export const HistoryPanel: React.FC = () => {
// 创建图像对象以获取尺寸
const img = new Image();
img.onload = () => {
// 计算文件大小仅对base64数据
let size = 0;
if (imageUrl.startsWith('data:')) {
// 估算base64数据大小
const base64Data = imageUrl.split(',')[1];
size = Math.round((base64Data.length * 3) / 4);
}
setHoveredImage({
url: imageUrl,
title: `生成记录 G${globalIndex + 1}`,
width: img.width,
height: img.height,
size: size
height: img.height
});
// 计算预览位置,确保不超出屏幕边界
const previewWidth = 500;
const previewHeight = 500;
const previewWidth = 300; // 减小预览窗口大小
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
@@ -596,13 +587,12 @@ export const HistoryPanel: React.FC = () => {
url: imageUrl,
title: `生成记录 G${globalIndex + 1}`,
width: 0,
height: 0,
size: 0
height: 0
});
// 计算预览位置
const previewWidth = 500;
const previewHeight = 500;
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
@@ -641,8 +631,8 @@ export const HistoryPanel: React.FC = () => {
}}
onMouseMove={(e) => {
// 调整预览位置以避免被遮挡
const previewWidth = 500;
const previewHeight = 500;
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
@@ -688,9 +678,9 @@ export const HistoryPanel: React.FC = () => {
}}
>
{(() => {
// 优先使用上传后的远程链接,如果没有则使用原始链接
// 优先使用上传后的远程链接
const imageUrl = getUploadedImageUrl(generation, 0) ||
(generation.outputAssets && generation.outputAssets.length > 0 ? generation.outputAssets[0].url : null);
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
if (imageUrl) {
return <img src={imageUrl} alt="生成的变体" className="w-full h-full object-cover" />;
@@ -757,25 +747,16 @@ export const HistoryPanel: React.FC = () => {
// 创建图像对象以获取尺寸
const img = new Image();
img.onload = () => {
// 计算文件大小仅对base64数据
let size = 0;
if (imageUrl.startsWith('data:')) {
// 估算base64数据大小
const base64Data = imageUrl.split(',')[1];
size = Math.round((base64Data.length * 3) / 4);
}
setHoveredImage({
url: imageUrl,
title: `编辑记录 E${globalIndex + 1}`,
width: img.width,
height: img.height,
size: size
height: img.height
});
// 计算预览位置,确保不超出屏幕边界
const previewWidth = 500;
const previewHeight = 500;
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
@@ -816,13 +797,12 @@ export const HistoryPanel: React.FC = () => {
url: imageUrl,
title: `编辑记录 E${globalIndex + 1}`,
width: 0,
height: 0,
size: 0
height: 0
});
// 计算预览位置
const previewWidth = 500;
const previewHeight = 500;
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
@@ -865,8 +845,8 @@ export const HistoryPanel: React.FC = () => {
}}
onMouseMove={(e) => {
// 调整预览位置以避免被遮挡
const previewWidth = 500;
const previewHeight = 500;
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
@@ -904,9 +884,9 @@ export const HistoryPanel: React.FC = () => {
}}
>
{(() => {
// 优先使用上传后的远程链接,如果没有则使用原始链接
// 优先使用上传后的远程链接
const imageUrl = getUploadedImageUrl(edit, 0) ||
(edit.outputAssets && edit.outputAssets.length > 0 ? edit.outputAssets[0].url : null);
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
if (imageUrl) {
return <img src={imageUrl} alt="编辑的变体" className="w-full h-full object-cover" />;
@@ -1042,7 +1022,7 @@ export const HistoryPanel: React.FC = () => {
// 获取上传后的远程链接(如果存在)
// 参考图像在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_50`
? `${gen.uploadResults[index + 1].url}?x-oss-process=image/quality,q_30`
: null;
const displayUrl = uploadedUrl || asset.url;
@@ -1150,7 +1130,7 @@ export const HistoryPanel: React.FC = () => {
// 获取上传后的远程链接(如果存在)
// 参考图像在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_50`
? `${parentGen.uploadResults[index + 1].url}?x-oss-process=image/quality,q_30`
: null;
const displayUrl = uploadedUrl || asset.url;
@@ -1219,7 +1199,25 @@ export const HistoryPanel: React.FC = () => {
if (imageUrl) {
// 处理数据URL和常规URL
if (imageUrl.startsWith('data:')) {
if (imageUrl.startsWith('data:') || imageUrl.startsWith('blob:')) {
// 对于Blob URL我们需要获取实际的Blob数据
if (imageUrl.startsWith('blob:')) {
// 从AppStore获取Blob
const blob = useAppStore.getState().getBlob(imageUrl);
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.revokeObjectURL(url);
return;
}
}
// 对于数据URL直接下载
const link = document.createElement('a');
link.href = imageUrl;
link.download = `nano-banana-${Date.now()}.png`;
@@ -1266,8 +1264,8 @@ export const HistoryPanel: React.FC = () => {
style={{
left: `${previewPosition.x}px`,
top: `${previewPosition.y}px`,
maxWidth: '300px',
maxHeight: '300px'
maxWidth: '200px', // 减小最大宽度
maxHeight: '200px'
}}
>
<div className="bg-gray-900 text-white text-xs p-2 truncate font-medium">
@@ -1276,7 +1274,7 @@ export const HistoryPanel: React.FC = () => {
<img
src={hoveredImage.url}
alt="预览"
className="w-full h-auto max-h-[200px] object-contain"
className="w-full h-auto max-h-[150px] object-contain"
/>
{/* 图像信息 */}
<div className="p-2 bg-gray-50 border-t border-gray-200 text-xs">
@@ -1286,12 +1284,6 @@ export const HistoryPanel: React.FC = () => {
<span className="text-gray-800">{hoveredImage.width} × {hoveredImage.height}</span>
</div>
)}
{hoveredImage.size > 0 && (
<div className="flex justify-between text-gray-600 mt-1">
<span>:</span>
<span className="text-gray-800">{Math.round(hoveredImage.size / 1024)} KB</span>
</div>
)}
</div>
</div>
)}

View File

@@ -77,6 +77,13 @@ export const ImageCanvas: React.FC = () => {
img.src = canvasImage;
} else {
// 当没有图像时,清理之前的图像对象
if (image) {
// 清理图像对象以释放内存
image.onload = null;
image.onerror = null;
image.src = '';
}
setImage(null);
}
@@ -86,9 +93,18 @@ export const ImageCanvas: React.FC = () => {
if (img) {
img.onload = null;
img.onerror = null;
// 清理图像源以释放内存
img.src = '';
}
// 清理之前的图像对象
if (image) {
image.onload = null;
image.onerror = null;
image.src = '';
}
};
}, [canvasImage, stageSize, setCanvasZoom, setCanvasPan]);
}, [canvasImage, stageSize, setCanvasZoom, setCanvasPan, image]);
// 处理舞台大小调整
useEffect(() => {

View File

@@ -4,7 +4,7 @@ import { Button } from './ui/Button';
import { useAppStore } from '../store/useAppStore';
import { useImageGeneration, useImageEditing } from '../hooks/useImageGeneration';
import { Upload, Wand2, Edit3, MousePointer, HelpCircle, ChevronDown, ChevronRight, RotateCcw } from 'lucide-react';
import { blobToBase64 } from '../utils/imageUtils';
import { blobToBase64, urlToBlob } from '../utils/imageUtils';
import { PromptHints } from './PromptHints';
import { cn } from '../utils/cn';
@@ -32,6 +32,7 @@ export const PromptComposer: React.FC = () => {
showPromptPanel,
setShowPromptPanel,
clearBrushStrokes,
addBlob
} = useAppStore();
const { generate, cancelGeneration } = useImageGeneration();
@@ -42,17 +43,45 @@ export const PromptComposer: React.FC = () => {
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleGenerate = () => {
const handleGenerate = async () => {
if (!currentPrompt.trim()) return;
if (selectedTool === 'generate') {
const referenceImages = uploadedImages
.filter(img => img.includes('base64,'))
.map(img => img.split('base64,')[1]);
// 将上传的图像转换为Blob对象
const referenceImageBlobs: Blob[] = [];
for (const img of uploadedImages) {
if (img.startsWith('data:')) {
// 从base64数据创建Blob
const base64 = img.split('base64,')[1];
const byteString = atob(base64);
const mimeString = img.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
referenceImageBlobs.push(new Blob([ab], { type: mimeString }));
} else if (img.startsWith('blob:')) {
// 从Blob URL获取Blob
const { getBlob } = useAppStore.getState();
const blob = getBlob(img);
if (blob) {
referenceImageBlobs.push(blob);
}
} else {
// 从URL获取Blob
try {
const blob = await urlToBlob(img);
referenceImageBlobs.push(blob);
} catch (error) {
console.warn('无法获取参考图像:', img, error);
}
}
}
generate({
prompt: currentPrompt,
referenceImages: referenceImages.length > 0 ? referenceImages : undefined,
referenceImages: referenceImageBlobs.length > 0 ? referenceImageBlobs : undefined,
temperature,
seed: seed !== null ? seed : undefined
});
@@ -64,28 +93,28 @@ export const PromptComposer: React.FC = () => {
const handleFileUpload = async (file: File) => {
if (file && file.type.startsWith('image/')) {
try {
const base64 = await blobToBase64(file);
const dataUrl = `data:${file.type};base64,${base64}`;
// 直接使用Blob创建URL
const blobUrl = addBlob(file);
if (selectedTool === 'generate') {
// 添加到参考图像最多2张
if (uploadedImages.length < 2) {
addUploadedImage(dataUrl);
addUploadedImage(blobUrl);
}
} else if (selectedTool === 'edit') {
// 编辑模式下添加到单独的编辑参考图像最多2张
if (editReferenceImages.length < 2) {
addEditReferenceImage(dataUrl);
addEditReferenceImage(blobUrl);
}
// 如果没有画布图像,则设置为画布图像
if (!canvasImage) {
setCanvasImage(dataUrl);
setCanvasImage(blobUrl);
}
} else if (selectedTool === 'mask') {
// 遮罩模式下,立即设置为画布图像
clearUploadedImages();
addUploadedImage(dataUrl);
setCanvasImage(dataUrl);
addUploadedImage(blobUrl);
setCanvasImage(blobUrl);
}
} catch (error) {
console.error('上传图像失败:', error);