diff --git a/src/App.tsx b/src/App.tsx index e2ea491..5dd6c40 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,16 @@ function AppContent() { }; init(); + + // 组件卸载时清理所有Blob URL + return () => { + const { blobStore } = useAppStore.getState(); + blobStore.forEach((blob, url) => { + if (url.startsWith('blob:')) { + URL.revokeObjectURL(url); + } + }); + }; }, []); // 在挂载时设置移动设备默认值 diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx index fb44300..973c760 100644 --- a/src/components/HistoryPanel.tsx +++ b/src/components/HistoryPanel.tsx @@ -612,11 +612,20 @@ export const HistoryPanel: React.FC<{ )} onClick={() => { selectGeneration(generation.id); - // 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产 + // 设置画布图像为生成结果图像,而不是参考图像 let imageUrl = null; - // 首先尝试获取参考图像 - if (generation.sourceAssets && generation.sourceAssets.length > 0) { + // 优先使用生成结果图像 + if (generation.outputAssets && generation.outputAssets.length > 0) { + const asset = generation.outputAssets[0]; + if (asset.url) { + const uploadedUrl = getUploadedImageUrl(generation, 0); + imageUrl = uploadedUrl || asset.url; + } + } + + // 如果没有生成结果图像,则使用参考图像 + if (!imageUrl && generation.sourceAssets && generation.sourceAssets.length > 0) { // 参考图像在uploadResults中从索引outputAssets.length开始 const outputAssetsCount = generation.outputAssets?.length || 0; const uploadResultIndex = outputAssetsCount; // 第一个参考图像 @@ -628,8 +637,57 @@ export const HistoryPanel: React.FC<{ } } - // 如果没有参考图像,则使用生成结果图像 - if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) { + if (imageUrl) { + // 检查是否是Blob URL并且可能已经失效 + if (imageUrl.startsWith('blob:')) { + // 预先检查Blob URL是否有效 + fetch(imageUrl) + .then(response => { + if (!response.ok) { + // Blob URL失效,尝试从AppStore重新获取 + const { getBlob } = useAppStore.getState(); + const blob = getBlob(imageUrl); + if (blob) { + console.log('从AppStore找到Blob,重新创建URL...'); + const newUrl = URL.createObjectURL(blob); + setCanvasImage(newUrl); + } else { + // 如果AppStore中也没有,直接设置原URL,让ImageCanvas处理 + setCanvasImage(imageUrl); + } + } else { + // Blob URL有效,直接使用 + setCanvasImage(imageUrl); + } + }) + .catch(() => { + // 网络错误,尝试从AppStore重新获取 + const { getBlob } = useAppStore.getState(); + const blob = getBlob(imageUrl); + if (blob) { + console.log('从AppStore找到Blob,重新创建URL...'); + const newUrl = URL.createObjectURL(blob); + setCanvasImage(newUrl); + } else { + // 如果AppStore中也没有,直接设置原URL,让ImageCanvas处理 + setCanvasImage(imageUrl); + } + }); + } else { + // 非Blob URL直接设置 + setCanvasImage(imageUrl); + } + } + }} + onMouseEnter={(e) => { + // 设置当前悬停的记录 + setHoveredRecord({type: 'generation', id: generation.id}); + + // 优先显示生成结果图像,如果没有生成结果图像则显示参考图像 + let imageUrl = null; + + // 优先使用生成结果图像 + if (generation.outputAssets && generation.outputAssets.length > 0) { const asset = generation.outputAssets[0]; if (asset.url) { const uploadedUrl = getUploadedImageUrl(generation, 0); @@ -637,19 +695,8 @@ export const HistoryPanel: React.FC<{ } } - if (imageUrl) { - setCanvasImage(imageUrl); - } - }} - onMouseEnter={(e) => { - // 设置当前悬停的记录 - setHoveredRecord({type: 'generation', id: generation.id}); - - // 优先显示参考图像,如果没有参考图像则显示生成结果图像 - let imageUrl = null; - - // 首先尝试获取参考图像 - if (generation.sourceAssets && generation.sourceAssets.length > 0) { + // 如果没有生成结果图像,则使用参考图像 + if (!imageUrl && generation.sourceAssets && generation.sourceAssets.length > 0) { // 参考图像在uploadResults中从索引outputAssets.length开始 const outputAssetsCount = generation.outputAssets?.length || 0; const uploadResultIndex = outputAssetsCount; // 第一个参考图像 @@ -657,12 +704,6 @@ export const HistoryPanel: React.FC<{ (generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null); } - // 如果没有参考图像,则使用生成结果图像 - if (!imageUrl) { - imageUrl = getUploadedImageUrl(generation, 0) || - (generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null); - } - if (imageUrl) { // 创建图像对象以获取尺寸 const img = new Image(); @@ -769,11 +810,20 @@ export const HistoryPanel: React.FC<{ }} > {(() => { - // 优先显示参考图像,如果没有参考图像则显示生成结果图像 + // 优先显示生成结果图像,如果没有生成结果图像则显示参考图像 let imageUrl = null; - // 首先尝试获取参考图像 - if (generation.sourceAssets && generation.sourceAssets.length > 0) { + // 优先使用生成结果图像 + if (generation.outputAssets && generation.outputAssets.length > 0) { + const asset = generation.outputAssets[0]; + if (asset.url) { + const uploadedUrl = getUploadedImageUrl(generation, 0); + imageUrl = uploadedUrl || asset.url; + } + } + + // 如果没有生成结果图像,则使用参考图像 + if (!imageUrl && generation.sourceAssets && generation.sourceAssets.length > 0) { // 参考图像在uploadResults中从索引outputAssets.length开始 const outputAssetsCount = generation.outputAssets?.length || 0; const uploadResultIndex = outputAssetsCount; // 第一个参考图像 @@ -781,12 +831,6 @@ export const HistoryPanel: React.FC<{ (generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null); } - // 如果没有参考图像,则使用生成结果图像 - if (!imageUrl) { - imageUrl = getUploadedImageUrl(generation, 0) || - (generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null); - } - if (imageUrl) { return 生成的变体; } else { @@ -919,11 +963,20 @@ export const HistoryPanel: React.FC<{ onClick={() => { selectEdit(edit.id); selectGeneration(null); - // 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产 + // 设置画布图像为编辑结果图像,而不是参考图像 let imageUrl = null; - // 首先尝试获取参考图像 - if (edit.sourceAssets && edit.sourceAssets.length > 0) { + // 优先使用编辑结果图像 + if (edit.outputAssets && edit.outputAssets.length > 0) { + const asset = edit.outputAssets[0]; + if (asset.url) { + const uploadedUrl = getUploadedImageUrl(edit, 0); + imageUrl = uploadedUrl || asset.url; + } + } + + // 如果没有编辑结果图像,则使用参考图像 + if (!imageUrl && edit.sourceAssets && edit.sourceAssets.length > 0) { // 参考图像在uploadResults中从索引outputAssets.length开始 const outputAssetsCount = edit.outputAssets?.length || 0; const uploadResultIndex = outputAssetsCount; // 第一个参考图像 @@ -935,8 +988,57 @@ export const HistoryPanel: React.FC<{ } } - // 如果没有参考图像,则使用编辑结果图像 - if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) { + if (imageUrl) { + // 检查是否是Blob URL并且可能已经失效 + if (imageUrl.startsWith('blob:')) { + // 预先检查Blob URL是否有效 + fetch(imageUrl) + .then(response => { + if (!response.ok) { + // Blob URL失效,尝试从AppStore重新获取 + const { getBlob } = useAppStore.getState(); + const blob = getBlob(imageUrl); + if (blob) { + console.log('从AppStore找到Blob,重新创建URL...'); + const newUrl = URL.createObjectURL(blob); + setCanvasImage(newUrl); + } else { + // 如果AppStore中也没有,直接设置原URL,让ImageCanvas处理 + setCanvasImage(imageUrl); + } + } else { + // Blob URL有效,直接使用 + setCanvasImage(imageUrl); + } + }) + .catch(() => { + // 网络错误,尝试从AppStore重新获取 + const { getBlob } = useAppStore.getState(); + const blob = getBlob(imageUrl); + if (blob) { + console.log('从AppStore找到Blob,重新创建URL...'); + const newUrl = URL.createObjectURL(blob); + setCanvasImage(newUrl); + } else { + // 如果AppStore中也没有,直接设置原URL,让ImageCanvas处理 + setCanvasImage(imageUrl); + } + }); + } else { + // 非Blob URL直接设置 + setCanvasImage(imageUrl); + } + } + }} + onMouseEnter={(e) => { + // 设置当前悬停的记录 + setHoveredRecord({type: 'edit', id: edit.id}); + + // 优先显示编辑结果图像,如果没有编辑结果图像则显示参考图像 + let imageUrl = null; + + // 优先使用编辑结果图像 + if (edit.outputAssets && edit.outputAssets.length > 0) { const asset = edit.outputAssets[0]; if (asset.url) { const uploadedUrl = getUploadedImageUrl(edit, 0); @@ -944,19 +1046,8 @@ export const HistoryPanel: React.FC<{ } } - if (imageUrl) { - setCanvasImage(imageUrl); - } - }} - onMouseEnter={(e) => { - // 设置当前悬停的记录 - setHoveredRecord({type: 'edit', id: edit.id}); - - // 优先显示参考图像,如果没有参考图像则显示编辑结果图像 - let imageUrl = null; - - // 首先尝试获取参考图像 - if (edit.sourceAssets && edit.sourceAssets.length > 0) { + // 如果没有编辑结果图像,则使用参考图像 + if (!imageUrl && edit.sourceAssets && edit.sourceAssets.length > 0) { // 参考图像在uploadResults中从索引outputAssets.length开始 const outputAssetsCount = edit.outputAssets?.length || 0; const uploadResultIndex = outputAssetsCount; // 第一个参考图像 @@ -964,12 +1055,6 @@ export const HistoryPanel: React.FC<{ (edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null); } - // 如果没有参考图像,则使用编辑结果图像 - if (!imageUrl) { - imageUrl = getUploadedImageUrl(edit, 0) || - (edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null); - } - if (imageUrl) { // 创建图像对象以获取尺寸 const img = new Image(); @@ -1016,11 +1101,20 @@ export const HistoryPanel: React.FC<{ }} > {(() => { - // 优先显示参考图像,如果没有参考图像则显示编辑结果图像 + // 优先显示编辑结果图像,如果没有编辑结果图像则显示参考图像 let imageUrl = null; - // 首先尝试获取参考图像 - if (edit.sourceAssets && edit.sourceAssets.length > 0) { + // 优先使用编辑结果图像 + if (edit.outputAssets && edit.outputAssets.length > 0) { + const asset = edit.outputAssets[0]; + if (asset.url) { + const uploadedUrl = getUploadedImageUrl(edit, 0); + imageUrl = uploadedUrl || asset.url; + } + } + + // 如果没有编辑结果图像,则使用参考图像 + if (!imageUrl && edit.sourceAssets && edit.sourceAssets.length > 0) { // 参考图像在uploadResults中从索引outputAssets.length开始 const outputAssetsCount = edit.outputAssets?.length || 0; const uploadResultIndex = outputAssetsCount; // 第一个参考图像 @@ -1028,12 +1122,6 @@ export const HistoryPanel: React.FC<{ (edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null); } - // 如果没有参考图像,则使用编辑结果图像 - if (!imageUrl) { - imageUrl = getUploadedImageUrl(edit, 0) || - (edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null); - } - if (imageUrl) { return 编辑的变体; } else { diff --git a/src/components/ImageCanvas.tsx b/src/components/ImageCanvas.tsx index b835585..f245b63 100644 --- a/src/components/ImageCanvas.tsx +++ b/src/components/ImageCanvas.tsx @@ -8,7 +8,9 @@ export const ImageCanvas: React.FC = () => { const { canvasImage, canvasZoom, + canvasPan, setCanvasZoom, + setCanvasPan, brushStrokes, addBrushStroke, showMasks, @@ -48,132 +50,194 @@ export const ImageCanvas: React.FC = () => { // 加载图像 useEffect(() => { - let img: HTMLImageElement | null = null; + console.log('useEffect triggered, canvasImage:', canvasImage); - if (canvasImage) { - console.log('开始加载图像,URL:', canvasImage); + // 如果没有图像URL,直接返回 + if (!canvasImage) { + console.log('没有图像需要加载'); + setImage(null); + return; + } + + let img: HTMLImageElement | null = null; + let isCancelled = false; + + console.log('开始加载图像,URL:', canvasImage); + + img = new window.Image(); + + img.onload = () => { + // 检查是否已取消 + if (isCancelled) { + console.log('图像加载被取消'); + return; + } - img = new window.Image(); - const isCancelled = false; + console.log('图像加载成功,尺寸:', img.width, 'x', img.height); + setImage(img); - img.onload = () => { - // 检查是否已取消 - if (isCancelled) { - console.log('图像加载被取消'); - return; - } + // 只在图像首次加载时自动适应画布 + if (!isCancelled && img) { + const isMobile = window.innerWidth < 768; + const padding = isMobile ? 0.9 : 0.8; - console.log('图像加载成功,尺寸:', img.width, 'x', img.height); - setImage(img); + const scaleX = (stageSize.width * padding) / img.width; + const scaleY = (stageSize.height * padding) / img.height; - // 只在图像首次加载时自动适应画布 - if (!isCancelled && img) { - const isMobile = window.innerWidth < 768; - const padding = isMobile ? 0.9 : 0.8; + const maxZoom = isMobile ? 0.3 : 0.8; + const optimalZoom = Math.min(scaleX, scaleY, maxZoom); + + // 立即更新React状态以确保Konva Image组件使用正确的缩放值 + setCanvasZoom(optimalZoom); + setCanvasPan({ x: 0, y: 0 }); + + // 使用setTimeout确保DOM已更新后再设置Stage + setTimeout(() => { + // 检查是否已取消 + if (isCancelled) { + return; + } - const scaleX = (stageSize.width * padding) / img.width; - const scaleY = (stageSize.height * padding) / img.height; - - const maxZoom = isMobile ? 0.3 : 0.8; - const optimalZoom = Math.min(scaleX, scaleY, maxZoom); - - // 立即更新React状态以确保Konva Image组件使用正确的缩放值 - setCanvasZoom(optimalZoom); - setCanvasPan({ x: 0, y: 0 }); - - // 使用setTimeout确保DOM已更新后再设置Stage - setTimeout(() => { - if (!isCancelled && img) { - // 直接设置缩放,但保持Stage居中 - const stage = stageRef.current; - if (stage) { - stage.scale({ x: optimalZoom, y: optimalZoom }); - // 重置Stage位置以确保居中 - stage.position({ x: 0, y: 0 }); - stage.batchDraw(); + if (!isCancelled && img) { + // 直接设置缩放,但保持Stage居中 + const stage = stageRef.current; + if (stage) { + stage.scale({ x: optimalZoom, y: optimalZoom }); + // 重置Stage位置以确保居中 + stage.position({ x: 0, y: 0 }); + stage.batchDraw(); + } + + console.log('图像自动适应画布完成,缩放:', optimalZoom); + } + }, 0); + } + }; + + img.onerror = (error) => { + // 检查是否已取消 + if (isCancelled) { + return; + } + + console.error('图像加载失败:', error); + console.error('图像URL:', canvasImage); + + // 检查是否是IndexedDB URL + if (canvasImage.startsWith('indexeddb://')) { + console.log('正在处理IndexedDB图像...'); + + // 从IndexedDB获取图像并创建Blob URL + const imageId = canvasImage.replace('indexeddb://', ''); + import('../services/referenceImageService').then((module) => { + const referenceImageService = module; + referenceImageService.getReferenceImage(imageId) + .then(blob => { + // 检查是否已取消 + if (isCancelled) { + return; } - console.log('图像自动适应画布完成,缩放:', optimalZoom); - } - }, 0); - } - }; - - img.onerror = (error) => { - if (!isCancelled) { - console.error('图像加载失败:', error); - console.error('图像URL:', canvasImage); + if (blob) { + const newUrl = URL.createObjectURL(blob); + console.log('从IndexedDB创建新的Blob URL:', newUrl); + // 更新canvasImage为新的URL + import('../store/useAppStore').then((storeModule) => { + const useAppStore = storeModule.useAppStore; + // 检查是否已取消 + if (!isCancelled) { + useAppStore.getState().setCanvasImage(newUrl); + } + }); + } else { + console.error('IndexedDB中未找到图像'); + } + }) + .catch(err => { + // 检查是否已取消 + if (isCancelled) { + return; + } + + console.error('从IndexedDB获取图像时出错:', err); + }); + }).catch(err => { + // 检查是否已取消 + if (isCancelled) { + return; + } - // 检查是否是Blob URL - if (canvasImage.startsWith('blob:')) { - console.log('正在检查Blob URL是否有效...'); + console.error('导入referenceImageService时出错:', err); + }); + } + // 检查是否是Blob URL + else if (canvasImage.startsWith('blob:')) { + console.log('正在检查Blob URL是否有效...'); + + // 尝试从AppStore重新获取Blob并创建新的URL + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(canvasImage); + if (blob) { + // 检查是否已取消 + if (isCancelled) { + return; + } - // 检查Blob URL是否仍然有效 + console.log('从AppStore找到Blob,尝试重新创建URL...'); + // 重新创建Blob URL并重试加载 + const newUrl = URL.createObjectURL(blob); + console.log('创建新的Blob URL:', newUrl); + // 更新canvasImage为新的URL + useAppStore.getState().setCanvasImage(newUrl); + } else { + // 检查是否已取消 + if (isCancelled) { + return; + } + + console.error('AppStore中未找到Blob'); + // 如果AppStore中也没有,尝试通过fetch检查URL fetch(canvasImage) .then(response => { + // 检查是否已取消 + if (isCancelled) { + return; + } + if (!response.ok) { console.error('Blob URL无法访问:', response.status, response.statusText); - // 尝试从AppStore重新获取Blob并创建新的URL - import('../store/useAppStore').then((module) => { - const useAppStore = module.useAppStore; - const blob = useAppStore.getState().getBlob(canvasImage); - if (blob) { - console.log('从AppStore找到Blob,尝试重新创建URL...'); - // 重新创建Blob URL并重试加载 - const newUrl = URL.createObjectURL(blob); - console.log('创建新的Blob URL:', newUrl); - // 更新canvasImage为新的URL - useAppStore.getState().setCanvasImage(newUrl); - } else { - console.error('AppStore中未找到Blob'); - } - }).catch(err => { - console.error('导入AppStore时出错:', err); - }); } else { - console.log('Blob URL可以访问'); + console.log('Blob URL可以访问,但图像加载仍然失败'); } }) - .catch(err => { - console.error('检查Blob URL时出错:', err); - // 尝试从AppStore重新获取Blob - import('../store/useAppStore').then((module) => { - const useAppStore = module.useAppStore; - const blob = useAppStore.getState().getBlob(canvasImage); - if (blob) { - console.log('从AppStore找到Blob,尝试重新创建URL...'); - // 重新创建Blob URL并重试加载 - const newUrl = URL.createObjectURL(blob); - console.log('创建新的Blob URL:', newUrl); - // 更新canvasImage为新的URL - useAppStore.getState().setCanvasImage(newUrl); - } else { - console.error('AppStore中未找到Blob'); - } - }).catch(err => { - console.error('导入AppStore时出错:', err); - }); + .catch(fetchErr => { + // 检查是否已取消 + if (isCancelled) { + return; + } + + console.error('检查Blob URL时出错:', fetchErr); }); } - } - }; - - img.src = canvasImage; - } else { - console.log('没有图像需要加载'); - // 当没有图像时,清理之前的图像对象 - if (image) { - // 清理图像对象以释放内存 - image.onload = null; - image.onerror = null; - image.src = ''; + }).catch(err => { + // 检查是否已取消 + if (isCancelled) { + return; + } + + console.error('导入AppStore时出错:', err); + }); } - setImage(null); - } + }; + + img.src = canvasImage; // 清理函数 return () => { console.log('清理图像加载资源'); + // 标记为已取消 + isCancelled = true; // 取消图像加载 if (img) { img.onload = null; @@ -181,15 +245,8 @@ export const ImageCanvas: React.FC = () => { // 清理图像源以释放内存 img.src = ''; } - - // 清理之前的图像对象 - if (image) { - image.onload = null; - image.onerror = null; - image.src = ''; - } }; - }, [canvasImage, image, setCanvasZoom, stageSize.height, stageSize.width]); // 添加所有依赖项 + }, [canvasImage, setCanvasZoom, setCanvasPan, stageSize.height, stageSize.width]); // 移除image依赖项 // 处理舞台大小调整 useEffect(() => { diff --git a/src/components/PromptComposer.tsx b/src/components/PromptComposer.tsx index 96e8281..0ae5738 100644 --- a/src/components/PromptComposer.tsx +++ b/src/components/PromptComposer.tsx @@ -16,7 +16,12 @@ const ImagePreview: React.FC<{ index: number; selectedTool: 'generate' | 'edit' | 'mask'; onRemove: () => void; -}> = ({ image, index, onRemove }) => { + onDragStart?: (e: React.DragEvent, index: number) => void; + onDragOver?: (e: React.DragEvent, index: number) => void; + onDragEnd?: (e: React.DragEvent) => void; + onDrop?: (e: React.DragEvent, index: number) => void; + isDragging?: boolean; +}> = ({ image, index, onRemove, onDragStart, onDragOver, onDragEnd, onDrop, isDragging }) => { const [imageSrc, setImageSrc] = useState(image); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); @@ -74,7 +79,20 @@ const ImagePreview: React.FC<{ } return ( -
+
onDragStart && onDragStart(e, index)} + onDragOver={(e) => { + e.preventDefault(); + onDragOver && onDragOver(e, index); + }} + onDragEnd={(e) => onDragEnd && onDragEnd(e)} + onDrop={(e) => { + e.preventDefault(); + onDrop && onDrop(e, index); + }} + > {`参考图像 { uploadedImages, addUploadedImage, removeUploadedImage, + reorderUploadedImage, clearUploadedImages, editReferenceImages, addEditReferenceImage, @@ -130,6 +149,7 @@ export const PromptComposer: React.FC = () => { const [showClearConfirm, setShowClearConfirm] = useState(false); const [showHintsModal, setShowHintsModal] = useState(false); const [isDragOver, setIsDragOver] = useState(false); + const [draggedIndex, setDraggedIndex] = useState(null); const fileInputRef = useRef(null); // 初始化参考图像数据库 @@ -237,6 +257,13 @@ export const PromptComposer: React.FC = () => { // 创建一个特殊的URL来标识这是存储在IndexedDB中的图像 const imageUrl = `indexeddb://${imageId}`; + // 同时创建一个可以直接在画布上显示的Blob URL + const blob = await referenceImageService.getReferenceImage(imageId); + let displayUrl = imageUrl; // 默认使用IndexedDB URL + if (blob) { + displayUrl = URL.createObjectURL(blob); + } + if (selectedTool === 'generate') { // 添加到参考图像(最多2张) if (uploadedImages.length < 2) { @@ -247,15 +274,15 @@ export const PromptComposer: React.FC = () => { if (editReferenceImages.length < 2) { addEditReferenceImage(imageUrl); } - // 如果没有画布图像,则设置为画布图像 + // 如果没有画布图像,则设置为画布图像(使用可以直接显示的URL) if (!canvasImage) { - setCanvasImage(imageUrl); + setCanvasImage(displayUrl); } } else if (selectedTool === 'mask') { // 遮罩模式下,将图像添加为参考图像而不是清除现有图像 - // 只有在没有画布图像时才设置为画布图像 + // 只有在没有画布图像时才设置为画布图像(使用可以直接显示的URL) if (!canvasImage) { - setCanvasImage(imageUrl); + setCanvasImage(displayUrl); } // 不清除现有的上传图像,而是将新图像添加为参考图像(如果还有空间) if (uploadedImages.length < 2) { @@ -294,6 +321,31 @@ export const PromptComposer: React.FC = () => { } }; + // 拖拽排序处理函数 + const handleDragStart = (e: React.DragEvent, index: number) => { + setDraggedIndex(index); + e.dataTransfer.effectAllowed = 'move'; + // 在Firefox中需要设置dataTransfer数据 + e.dataTransfer.setData('text/plain', index.toString()); + }; + + const handleDragOverPreview = (e: React.DragEvent, index: number) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + }; + + const handleDropPreview = (e: React.DragEvent, index: number) => { + e.preventDefault(); + if (draggedIndex !== null && draggedIndex !== index) { + reorderUploadedImage(draggedIndex, index); + } + setDraggedIndex(null); + }; + + const handleDragEnd = () => { + setDraggedIndex(null); + }; + const handleClearSession = async () => { setCurrentPrompt(''); clearUploadedImages(); @@ -311,6 +363,9 @@ export const PromptComposer: React.FC = () => { } catch (error) { console.error('清空IndexedDB中的参考图像失败:', error); } + + // 清理所有Blob URL + useAppStore.getState().cleanupAllBlobUrls(); }; const tools = [ @@ -461,6 +516,11 @@ export const PromptComposer: React.FC = () => { index={index} selectedTool={selectedTool} onRemove={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)} + onDragStart={selectedTool === 'generate' ? handleDragStart : undefined} + onDragOver={selectedTool === 'generate' ? handleDragOverPreview : undefined} + onDragEnd={selectedTool === 'generate' ? handleDragEnd : undefined} + onDrop={selectedTool === 'generate' ? handleDropPreview : undefined} + isDragging={selectedTool === 'generate' && draggedIndex === index} /> ))}
diff --git a/src/hooks/useImageGeneration.ts b/src/hooks/useImageGeneration.ts index fbbac27..fea116e 100644 --- a/src/hooks/useImageGeneration.ts +++ b/src/hooks/useImageGeneration.ts @@ -208,7 +208,15 @@ export const useImageGeneration = () => { }; addGeneration(generation); - setCanvasImage(outputAssets[0].url); + + // 调试日志:检查outputAssets + console.log('生成完成,outputAssets:', outputAssets); + if (outputAssets && outputAssets.length > 0) { + console.log('第一个输出资产URL:', outputAssets[0].url); + setCanvasImage(outputAssets[0].url); + } else { + console.error('生成完成但没有输出资产'); + } // 自动选择新生成的记录 const { selectGeneration } = useAppStore.getState(); diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index afcf47c..9acfa7c 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -94,6 +94,7 @@ interface AppState { addUploadedImage: (url: string) => void; removeUploadedImage: (index: number) => void; + reorderUploadedImage: (fromIndex: number, toIndex: number) => void; clearUploadedImages: () => void; addEditReferenceImage: (url: string) => void; @@ -194,6 +195,12 @@ export const useAppStore = create()( uploadedImages: state.uploadedImages.filter((_, i) => i !== index) }; }), + reorderUploadedImage: (fromIndex, toIndex) => set((state) => { + const newUploadedImages = [...state.uploadedImages]; + const [movedItem] = newUploadedImages.splice(fromIndex, 1); + newUploadedImages.splice(toIndex, 0, movedItem); + return { uploadedImages: newUploadedImages }; + }), clearUploadedImages: () => set((state) => { // 删除所有存储在IndexedDB中的参考图像 state.uploadedImages.forEach(imageUrl => { @@ -557,19 +564,27 @@ export const useAppStore = create()( }), // 释放指定的Blob URLs - revokeBlobUrls: () => set((state) => { - // 不再自动清理Blob URL,以确保参考图像不会被意外删除 - // 只有在用户明确请求清除会话时才清理 - console.log('Blob清理已禁用,参考图像将被永久保留'); - return state; + revokeBlobUrls: (urls: string[]) => set((state) => { + // 清理指定的Blob URL + const newBlobStore = new Map(state.blobStore); + urls.forEach(url => { + if (url.startsWith('blob:')) { + URL.revokeObjectURL(url); + newBlobStore.delete(url); + } + }); + return { blobStore: newBlobStore }; }), // 释放所有Blob URLs cleanupAllBlobUrls: () => set((state) => { - // 不再自动清理Blob URL,以确保参考图像不会被意外删除 - // 只有在用户明确请求清除会话时才清理 - console.log('Blob清理已禁用,参考图像将被永久保留'); - return state; + // 清理所有Blob URL + state.blobStore.forEach((blob, url) => { + if (url.startsWith('blob:')) { + URL.revokeObjectURL(url); + } + }); + return { blobStore: new Map() }; }), // 定期清理Blob URL