From af2058f7528994f25a51680be406fae2dc61e6d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E6=B6=9B?= Date: Sat, 20 Sep 2025 00:38:35 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/HistoryPanel.tsx | 345 +++++++++++++++++++++++++++--- src/components/ImageCanvas.tsx | 17 ++ src/components/PromptComposer.tsx | 21 ++ src/hooks/useImageGeneration.ts | 28 +++ src/store/useAppStore.ts | 19 +- 5 files changed, 397 insertions(+), 33 deletions(-) diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx index dd2c7e2..6840157 100644 --- a/src/components/HistoryPanel.tsx +++ b/src/components/HistoryPanel.tsx @@ -630,16 +630,76 @@ export const HistoryPanel: React.FC<{ }; img.onerror = (error) => { console.error('图像加载失败:', error); - // 即使图像加载失败,也显示预览 - setHoveredImage({ - url: imageUrl, - title: `生成记录 G${globalIndex + 1}`, - width: 0, - height: 0 - }); - // 传递鼠标位置信息给App组件 - if (setPreviewPosition) { - setPreviewPosition({ x: e.clientX, y: e.clientY }); + // 如果是Blob URL失效,尝试重新获取 + if (imageUrl.startsWith('blob:')) { + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(imageUrl); + if (blob) { + console.log('从AppStore找到Blob,尝试重新创建URL...'); + // 重新创建Blob URL + const newUrl = URL.createObjectURL(blob); + // 更新显示 + setHoveredImage({ + url: newUrl, + title: `生成记录 G${globalIndex + 1}`, + width: 0, + height: 0 + }); + // 传递鼠标位置信息给App组件 + if (setPreviewPosition) { + setPreviewPosition({ x: e.clientX, y: e.clientY }); + } + // 预加载新URL + const newImg = new Image(); + newImg.onload = () => { + setHoveredImage({ + url: newUrl, + title: `生成记录 G${globalIndex + 1}`, + width: newImg.width, + height: newImg.height + }); + }; + newImg.src = newUrl; + } else { + // 即使图像加载失败,也显示预览 + setHoveredImage({ + url: imageUrl, + title: `生成记录 G${globalIndex + 1}`, + width: 0, + height: 0 + }); + // 传递鼠标位置信息给App组件 + if (setPreviewPosition) { + setPreviewPosition({ x: e.clientX, y: e.clientY }); + } + } + }).catch(err => { + console.error('导入AppStore时出错:', err); + // 即使图像加载失败,也显示预览 + setHoveredImage({ + url: imageUrl, + title: `生成记录 G${globalIndex + 1}`, + width: 0, + height: 0 + }); + // 传递鼠标位置信息给App组件 + if (setPreviewPosition) { + setPreviewPosition({ x: e.clientX, y: e.clientY }); + } + }); + } else { + // 即使图像加载失败,也显示预览 + setHoveredImage({ + url: imageUrl, + title: `生成记录 G${globalIndex + 1}`, + width: 0, + height: 0 + }); + // 传递鼠标位置信息给App组件 + if (setPreviewPosition) { + setPreviewPosition({ x: e.clientX, y: e.clientY }); + } } }; img.src = imageUrl; @@ -1026,7 +1086,30 @@ export const HistoryPanel: React.FC<{ ? `${gen.uploadResults[index].url}?x-oss-process=image/quality,q_30` : null; - const displayUrl = uploadedUrl || asset.url; + // 如果没有上传的URL,则使用asset中的URL + const displayUrl = uploadedUrl || asset.url || asset.blobUrl; + + // 如果URL是blob:开头但已失效,尝试重新创建 + if (displayUrl && displayUrl.startsWith('blob:')) { + // 检查blob是否仍然有效 + const img = new Image(); + img.onerror = () => { + // Blob URL可能已失效,尝试重新创建 + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + // 更新显示 + const imgElement = document.querySelector(`img[src="${displayUrl}"]`); + if (imgElement) { + imgElement.src = newUrl; + } + } + }); + }; + img.src = displayUrl; + } return (
- {`生成结果 + {displayUrl ? ( + {`生成结果 { + // 如果图像加载失败,尝试重新创建Blob URL + if (displayUrl.startsWith('blob:')) { + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + (e.target as HTMLImageElement).src = newUrl; + } + }); + } + }} + /> + ) : ( +
+ +
+ )}
); })} @@ -1074,11 +1176,36 @@ export const HistoryPanel: React.FC<{ const fullGen = dbGenerations.find(item => item.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` + // 确保索引在有效范围内 + const uploadResultIndex = outputAssetsCount + index; + const uploadedUrl = fullGen.uploadResults && fullGen.uploadResults[uploadResultIndex] && fullGen.uploadResults[uploadResultIndex].success + ? `${fullGen.uploadResults[uploadResultIndex].url}?x-oss-process=image/quality,q_30` : null; - const displayUrl = uploadedUrl || asset.url; + // 如果没有上传的URL,则使用asset中的URL + const displayUrl = uploadedUrl || asset.url || asset.blobUrl; + + // 如果URL是blob:开头但已失效,尝试重新创建 + if (displayUrl && displayUrl.startsWith('blob:')) { + // 检查blob是否仍然有效 + const img = new Image(); + img.onerror = () => { + // Blob URL可能已失效,尝试重新创建 + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + // 更新显示 + const imgElement = document.querySelector(`img[src="${displayUrl}"]`); + if (imgElement) { + imgElement.src = newUrl; + } + } + }); + }; + img.src = displayUrl; + } return (
- {`参考图像 + {displayUrl ? ( + {`参考图像 { + // 如果图像加载失败,尝试重新创建Blob URL + if (displayUrl.startsWith('blob:')) { + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + (e.target as HTMLImageElement).src = newUrl; + } + }); + } + }} + /> + ) : ( +
+ +
+ )}
); })} @@ -1166,6 +1312,95 @@ export const HistoryPanel: React.FC<{ )} + {/* 编辑结果图像 */} + {selectedEdit.outputAssets && selectedEdit.outputAssets.length > 0 && ( +
+
编辑结果
+
+ {selectedEdit.outputAssets.length} 个编辑结果 +
+
+ {selectedEdit.outputAssets.slice(0, 4).map((asset: any, index: number) => { + // 获取上传后的远程链接(如果存在) + const uploadedUrl = selectedEdit.uploadResults && selectedEdit.uploadResults[index] && selectedEdit.uploadResults[index].success + ? `${selectedEdit.uploadResults[index].url}?x-oss-process=image/quality,q_30` + : null; + + // 如果没有上传的URL,则使用asset中的URL + const displayUrl = uploadedUrl || asset.url || asset.blobUrl; + + // 如果URL是blob:开头但已失效,尝试重新创建 + if (displayUrl && displayUrl.startsWith('blob:')) { + // 检查blob是否仍然有效 + const img = new Image(); + img.onerror = () => { + // Blob URL可能已失效,尝试重新创建 + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + // 更新显示 + const imgElement = document.querySelector(`img[src="${displayUrl}"]`); + if (imgElement) { + imgElement.src = newUrl; + } + } + }); + }; + img.src = displayUrl; + } + + return ( +
{ + e.stopPropagation(); + setPreviewModal({ + open: true, + imageUrl: displayUrl, + title: `编辑结果 ${index + 1}`, + description: `${asset.width} × ${asset.height}` + }); + }} + > + {displayUrl ? ( + {`编辑结果 { + // 如果图像加载失败,尝试重新创建Blob URL + if (displayUrl.startsWith('blob:')) { + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + (e.target as HTMLImageElement).src = newUrl; + } + }); + } + }} + /> + ) : ( +
+ +
+ )} +
+ ); + })} + {selectedEdit.outputAssets.length > 4 && ( +
+ +{selectedEdit.outputAssets.length - 4} +
+ )} +
+
+ )} + {/* 原始生成参考 */} {parentGen && (
@@ -1184,10 +1419,37 @@ export const HistoryPanel: React.FC<{ // 获取上传后的远程链接(如果存在) // 参考图像在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` + + // 确保索引在有效范围内 + const uploadResultIndex = outputAssetsCount + index; + const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[uploadResultIndex] && parentGen.uploadResults[uploadResultIndex].success + ? `${parentGen.uploadResults[uploadResultIndex].url}?x-oss-process=image/quality,q_30` : null; - const displayUrl = uploadedUrl || asset.url; + + // 如果没有上传的URL,则使用asset中的URL + const displayUrl = uploadedUrl || asset.url || asset.blobUrl; + + // 如果URL是blob:开头但已失效,尝试重新创建 + if (displayUrl && displayUrl.startsWith('blob:')) { + // 检查blob是否仍然有效 + const img = new Image(); + img.onerror = () => { + // Blob URL可能已失效,尝试重新创建 + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + // 更新显示 + const imgElement = document.querySelector(`img[src="${displayUrl}"]`); + if (imgElement) { + imgElement.src = newUrl; + } + } + }); + }; + img.src = displayUrl; + } return (
- {`原始参考图像 + {displayUrl ? ( + {`原始参考图像 { + // 如果图像加载失败,尝试重新创建Blob URL + if (displayUrl.startsWith('blob:')) { + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + (e.target as HTMLImageElement).src = newUrl; + } + }); + } + }} + /> + ) : ( +
+ +
+ )}
); })} diff --git a/src/components/ImageCanvas.tsx b/src/components/ImageCanvas.tsx index 10e6f97..a79d6bd 100644 --- a/src/components/ImageCanvas.tsx +++ b/src/components/ImageCanvas.tsx @@ -119,6 +119,23 @@ export const ImageCanvas: React.FC = () => { .then(response => { 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可以访问'); } diff --git a/src/components/PromptComposer.tsx b/src/components/PromptComposer.tsx index 6865fae..6f0dbd8 100644 --- a/src/components/PromptComposer.tsx +++ b/src/components/PromptComposer.tsx @@ -69,6 +69,27 @@ export const PromptComposer: React.FC = () => { const blob = getBlob(img); if (blob) { referenceImageBlobs.push(blob); + } else { + // 如果在AppStore中找不到Blob,尝试重新创建 + try { + const response = await fetch(img); + if (response.ok) { + const blob = await response.blob(); + // 重新添加到AppStore + const newUrl = useAppStore.getState().addBlob(blob); + referenceImageBlobs.push(blob); + // 更新uploadedImages中的URL + const index = uploadedImages.indexOf(img); + if (index !== -1) { + const newImages = [...uploadedImages]; + newImages[index] = newUrl; + useAppStore.getState().clearUploadedImages(); + newImages.forEach(imageUrl => useAppStore.getState().addUploadedImage(imageUrl)); + } + } + } catch (error) { + console.warn('无法重新获取参考图像:', img, error); + } } } else { // 从URL获取Blob diff --git a/src/hooks/useImageGeneration.ts b/src/hooks/useImageGeneration.ts index 0411cfe..6d4659d 100644 --- a/src/hooks/useImageGeneration.ts +++ b/src/hooks/useImageGeneration.ts @@ -275,6 +275,34 @@ export const useImageEditing = () => { const blob = useAppStore.getState().getBlob(img); if (blob) { referenceImageBlobs.push(blob); + } else { + // 如果在AppStore中找不到Blob,尝试重新获取 + try { + const response = await fetch(img); + if (response.ok) { + const blob = await response.blob(); + referenceImageBlobs.push(blob); + // 重新添加到AppStore + const newUrl = useAppStore.getState().addBlob(blob); + // 更新editReferenceImages中的URL + const index = editReferenceImages.indexOf(img); + if (index !== -1) { + const { removeEditReferenceImage, addEditReferenceImage } = useAppStore.getState(); + removeEditReferenceImage(index); + // 重新添加所有图像以保持顺序 + const currentImages = [...editReferenceImages]; + currentImages[index] = newUrl; + // 清空并重新添加 + for (let i = 0; i < currentImages.length; i++) { + if (i < 2) { // 最多2张参考图像 + addEditReferenceImage(currentImages[i]); + } + } + } + } + } catch (error) { + console.warn('无法重新获取参考图像:', img, error); + } } } else if (img.includes('base64,')) { // 从base64数据创建Blob diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index c635875..e9bf353 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -637,8 +637,25 @@ export const useAppStore = create()( const isUsedInUploads = state.uploadedImages.includes(url); const isUsedInEdits = state.editReferenceImages.includes(url); + // 检查是否是历史记录中的参考图像 + const isUsedAsReference = state.currentProject && ( + state.currentProject.generations.some(gen => + gen.sourceAssets.some(asset => asset.blobUrl === url) + ) || + state.currentProject.edits.some(edit => + (edit.maskReferenceAssetBlobUrl === url) + ) + ); + + // 检查是否是当前编辑操作中的参考图像 + const isUsedInCurrentEdit = state.editReferenceImages.includes(url); + + // 检查是否是当前生成操作中的参考图像 + const isUsedInCurrentGeneration = state.uploadedImages.includes(url); + // 如果Blob没有被使用,则清理它 - if (!isUsedInProject && !isUsedInCanvas && !isUsedInUploads && !isUsedInEdits) { + // 但保留仍在作为参考图像使用的Blob + if (!isUsedInProject && !isUsedInCanvas && !isUsedInUploads && !isUsedInEdits && !isUsedAsReference && !isUsedInCurrentEdit && !isUsedInCurrentGeneration) { URL.revokeObjectURL(url); const newBlobStore = new Map(state.blobStore); newBlobStore.delete(url); From 690a5300316a829dc7d333676286d4a40a8ad084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E6=B6=9B?= Date: Sun, 21 Sep 2025 14:43:59 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E9=98=B6=E6=AE=B5=E6=80=A7=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.ts | 26 -- package-lock.json | 9 +- src/App.tsx | 11 +- src/components/HistoryPanel.tsx | 413 +++++++++++++++++----- src/components/ImageCanvas.tsx | 288 +++++---------- src/components/PromptComposer.tsx | 211 ++++++++--- src/components/PromptSuggestions.tsx | 14 +- src/components/ToastContext.tsx | 2 +- src/components/ui/Input.tsx | 34 +- src/components/ui/Textarea.tsx | 6 +- src/hooks/useImageGeneration.ts | 151 ++++++-- src/hooks/useIndexedDBListener.ts | 5 +- src/hooks/useKeyboardShortcuts.ts | 153 ++++++-- src/services/cacheService.ts | 4 +- src/services/geminiService.ts | 482 +++++++++++++++++++++----- src/services/indexedDBService.ts | 111 +++++- src/services/referenceImageService.ts | 114 ++++++ src/store/useAppStore.ts | 321 +++++------------ src/types/index.ts | 1 + src/utils/imageUtils.ts | 2 +- 20 files changed, 1577 insertions(+), 781 deletions(-) delete mode 100644 jest.config.ts create mode 100644 src/services/referenceImageService.ts diff --git a/jest.config.ts b/jest.config.ts deleted file mode 100644 index 3239d9f..0000000 --- a/jest.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Config } from 'jest'; - -const config: Config = { - preset: 'ts-jest', - testEnvironment: 'jsdom', - setupFilesAfterEnv: ['/src/__tests__/setup.ts'], - moduleNameMapper: { - '\.(css|less|scss|sass)$': 'identity-obj-proxy', - '^@/(.*)$': '/src/$1' - }, - testMatch: [ - '/src/__tests__/**/*.{ts,tsx}', - '/src/**/*.{spec,test}.{ts,tsx}' - ], - transform: { - '^.+\.(ts|tsx)$': 'ts-jest' - }, - collectCoverageFrom: [ - 'src/**/*.{ts,tsx}', - '!src/**/*.d.ts', - '!src/main.tsx', - '!src/vite-env.d.ts' - ] -}; - -export default config; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 36638d2..4528849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4456,9 +4456,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001667", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", - "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", "dev": true, "funding": [ { @@ -4473,7 +4473,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/canvas": { "version": "3.2.0", diff --git a/src/App.tsx b/src/App.tsx index eafda7d..e2ea491 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useReducer } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { cn } from './utils/cn'; import { Header } from './components/Header'; @@ -65,15 +65,6 @@ function AppContent() { return () => clearInterval(interval); }, []); - - // 定期清理未使用的Blob URL - useEffect(() => { - const interval = setInterval(() => { - useAppStore.getState().scheduleBlobCleanup(); - }, 60000); // 每分钟清理一次 - - return () => clearInterval(interval); - }, []); // 控制预览窗口的显示和隐藏动画 useEffect(() => { diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx index 6840157..fb44300 100644 --- a/src/components/HistoryPanel.tsx +++ b/src/components/HistoryPanel.tsx @@ -4,7 +4,6 @@ import { Button } from './ui/Button'; 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'; import { useIndexedDBListener } from '../hooks/useIndexedDBListener'; import { DayPicker } from 'react-day-picker'; import zhCN from 'react-day-picker/dist/locale/zh-CN'; @@ -13,9 +12,8 @@ export const HistoryPanel: React.FC<{ setHoveredImage: (image: {url: string, title: string, width?: number, height?: number} | null) => void, setPreviewPosition?: (position: {x: number, y: number} | null) => void }> = ({ setHoveredImage, setPreviewPosition }) => { - const { + const { currentProject, - canvasImage, selectedGenerationId, selectedEditId, selectGeneration, @@ -23,7 +21,6 @@ export const HistoryPanel: React.FC<{ showHistory, setShowHistory, setCanvasImage, - selectedTool, removeGeneration, removeEdit } = useAppStore(); @@ -68,18 +65,28 @@ export const HistoryPanel: React.FC<{ const [startDate, setStartDate] = useState(() => { // 初始化时默认显示今天的记录 const today = new Date(); + today.setHours(0, 0, 0, 0); return today.toISOString().split('T')[0]; }); const [endDate, setEndDate] = useState(() => { // 初始化时默认显示今天的记录 const today = new Date(); + today.setHours(0, 0, 0, 0); return today.toISOString().split('T')[0]; }); const [searchTerm, setSearchTerm] = useState(''); const [showDatePicker, setShowDatePicker] = useState(false); // 控制日期选择器的显示 const [dateRange, setDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({ - from: new Date(new Date().setHours(0, 0, 0, 0)), - to: new Date(new Date().setHours(0, 0, 0, 0)) + from: (() => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return today; + })(), + to: (() => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return today; + })() }); // 分页状态 @@ -87,17 +94,18 @@ export const HistoryPanel: React.FC<{ const itemsPerPage = 20; // 减少每页显示的项目数 // 获取当前图像尺寸 - const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null); + // const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null); // 当currentProject为空时,使用dbGenerations和dbEdits作为备选 const displayGenerations = currentProject ? currentProject.generations : dbGenerations; const displayEdits = currentProject ? currentProject.edits : dbEdits; // 筛选记录的函数 - const filterRecords = useCallback((records: any[], isGeneration: boolean) => { + const filterRecords = useCallback((records: Array<{timestamp: number, prompt?: string, instruction?: string}>, isGeneration: boolean) => { return records.filter(record => { // 日期筛选 - 检查记录日期是否在筛选范围内 const recordDate = new Date(record.timestamp); + recordDate.setHours(0, 0, 0, 0); // 设置为当天的开始 const recordDateStr = recordDate.toISOString().split('T')[0]; // 获取日期部分 YYYY-MM-DD // 检查是否在日期范围内 @@ -108,10 +116,10 @@ export const HistoryPanel: React.FC<{ if (searchTerm) { if (isGeneration) { // 生成记录按提示词搜索 - return record.prompt.toLowerCase().includes(searchTerm.toLowerCase()); + return record.prompt?.toLowerCase().includes(searchTerm.toLowerCase()) || false; } else { // 编辑记录按指令搜索 - return record.instruction.toLowerCase().includes(searchTerm.toLowerCase()); + return record.instruction?.toLowerCase().includes(searchTerm.toLowerCase()) || false; } } @@ -123,17 +131,17 @@ export const HistoryPanel: React.FC<{ const filteredGenerations = useMemo(() => filterRecords(displayGenerations, true), [displayGenerations, filterRecords]); const filteredEdits = useMemo(() => filterRecords(displayEdits, false), [displayEdits, filterRecords]); - React.useEffect(() => { - if (canvasImage) { - const img = new Image(); - img.onload = () => { - setImageDimensions({ width: img.width, height: img.height }); - }; - img.src = canvasImage; - } else { - setImageDimensions(null); - } - }, [canvasImage]); + // React.useEffect(() => { + // if (canvasImage) { + // const img = new Image(); + // img.onload = () => { + // setImageDimensions({ width: img.width, height: img.height }); + // }; + // img.src = canvasImage; + // } else { + // setImageDimensions(null); + // } + // }, [canvasImage]); // 当项目变化时,解码Blob图像 useEffect(() => { @@ -196,8 +204,8 @@ export const HistoryPanel: React.FC<{ if (generationOrEdit.uploadResults && generationOrEdit.uploadResults[index]) { const uploadResult = generationOrEdit.uploadResults[index]; if (uploadResult.success && uploadResult.url) { - // 添加参数以降低图片质量 - return `${uploadResult.url}?x-oss-process=image/quality,q_30`; // 降低质量到30% + // 返回原始链接,不添加任何参数 + return uploadResult.url; } } return null; @@ -469,6 +477,13 @@ export const HistoryPanel: React.FC<{ selected={dateRange} onSelect={(range) => { if (range) { + // 确保日期时间设置为当天的开始 + if (range.from) { + range.from.setHours(0, 0, 0, 0); + } + if (range.to) { + range.to.setHours(0, 0, 0, 0); + } setDateRange(range); // 更新字符串格式的日期用于筛选 if (range.from) { @@ -511,7 +526,8 @@ export const HistoryPanel: React.FC<{ className="text-xs p-1.5 rounded-l-none h-7 border-gray-200 text-gray-600 hover:bg-gray-100 card" onClick={() => { // 重置为显示今天的记录 - const today = new Date(new Date().setHours(0, 0, 0, 0)); + const today = new Date(); + today.setHours(0, 0, 0, 0); const todayStr = today.toISOString().split('T')[0]; setStartDate(todayStr); setEndDate(todayStr); @@ -581,7 +597,7 @@ export const HistoryPanel: React.FC<{ sortedGenerations.some(gen => gen.id === record.id) ); - return paginatedGenerations.map((generation, index) => { + return paginatedGenerations.map((generation: {id: string, sourceAssets?: Array<{url: string}>, outputAssets?: Array<{url: string}>}) => { // 计算全局索引用于显示编号 const globalIndex = allRecords.findIndex(record => record.id === generation.id); @@ -596,23 +612,57 @@ export const HistoryPanel: React.FC<{ )} onClick={() => { selectGeneration(generation.id); - // 设置画布图像为第一个输出资产 - if (generation.outputAssets && generation.outputAssets.length > 0) { + // 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产 + let imageUrl = null; + + // 首先尝试获取参考图像 + if (generation.sourceAssets && generation.sourceAssets.length > 0) { + // 参考图像在uploadResults中从索引outputAssets.length开始 + const outputAssetsCount = generation.outputAssets?.length || 0; + const uploadResultIndex = outputAssetsCount; // 第一个参考图像 + const uploadedUrl = getUploadedImageUrl(generation, uploadResultIndex); + if (uploadedUrl) { + imageUrl = uploadedUrl; + } else if (generation.sourceAssets[0].url) { + imageUrl = generation.sourceAssets[0].url; + } + } + + // 如果没有参考图像,则使用生成结果图像 + if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) { const asset = generation.outputAssets[0]; if (asset.url) { - setCanvasImage(asset.url); + const uploadedUrl = getUploadedImageUrl(generation, 0); + imageUrl = uploadedUrl || asset.url; } } + + if (imageUrl) { + setCanvasImage(imageUrl); + } }} onMouseEnter={(e) => { // 设置当前悬停的记录 setHoveredRecord({type: 'generation', id: generation.id}); - // 优先使用上传后的远程链接,如果没有则使用原始链接 - let imageUrl = getUploadedImageUrl(generation, 0); - if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) { - imageUrl = generation.outputAssets[0].url; - } + // 优先显示参考图像,如果没有参考图像则显示生成结果图像 + let imageUrl = null; + + // 首先尝试获取参考图像 + if (generation.sourceAssets && generation.sourceAssets.length > 0) { + // 参考图像在uploadResults中从索引outputAssets.length开始 + const outputAssetsCount = generation.outputAssets?.length || 0; + const uploadResultIndex = outputAssetsCount; // 第一个参考图像 + imageUrl = getUploadedImageUrl(generation, uploadResultIndex) || + (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(); @@ -719,9 +769,23 @@ export const HistoryPanel: React.FC<{ }} > {(() => { - // 优先使用上传后的远程链接 - const imageUrl = getUploadedImageUrl(generation, 0) || - (generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null); + // 优先显示参考图像,如果没有参考图像则显示生成结果图像 + let imageUrl = null; + + // 首先尝试获取参考图像 + if (generation.sourceAssets && generation.sourceAssets.length > 0) { + // 参考图像在uploadResults中从索引outputAssets.length开始 + const outputAssetsCount = generation.outputAssets?.length || 0; + const uploadResultIndex = outputAssetsCount; // 第一个参考图像 + imageUrl = getUploadedImageUrl(generation, uploadResultIndex) || + (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 生成的变体; @@ -748,12 +812,34 @@ export const HistoryPanel: React.FC<{ className="h-8 w-8 bg-white/90 hover:bg-white text-gray-700 rounded-full shadow-md" onClick={(e) => { e.stopPropagation(); - // 下载图像 - const imageUrl = getUploadedImageUrl(generation, 0) || - (generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null); + // 下载图像 - 优先下载参考图像,如果没有则下载生成结果图像 + let imageUrl = null; + + // 首先尝试获取参考图像 + if (generation.sourceAssets && generation.sourceAssets.length > 0) { + // 参考图像在uploadResults中从索引outputAssets.length开始 + const outputAssetsCount = generation.outputAssets?.length || 0; + const uploadResultIndex = outputAssetsCount; // 第一个参考图像 + imageUrl = getUploadedImageUrl(generation, uploadResultIndex) || + (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) { - // 使用Promise来处理异步操作 - fetch(imageUrl) + // 使用fetch获取图像数据并创建Blob URL以确保正确下载 + // 添加更多缓存控制头以绕过CDN缓存 + fetch(imageUrl, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }) .then(response => response.blob()) .then(blob => { const url = URL.createObjectURL(blob); @@ -767,6 +853,14 @@ export const HistoryPanel: React.FC<{ }) .catch(error => { console.error('下载图像失败:', error); + // 如果fetch失败,回退到直接使用a标签 + const link = document.createElement('a'); + link.href = imageUrl; + link.download = `nano-banana-${Date.now()}.png`; + link.target = '_blank'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); }); } }} @@ -809,7 +903,7 @@ export const HistoryPanel: React.FC<{ sortedEdits.some(edit => edit.id === record.id) ); - return paginatedEdits.map((edit, index) => { + return paginatedEdits.map((edit: {id: string, sourceAssets?: Array<{url: string}>, outputAssets?: Array<{url: string}>}) => { // 计算全局索引用于显示编号 const globalIndex = allRecords.findIndex(record => record.id === edit.id); @@ -825,22 +919,55 @@ export const HistoryPanel: React.FC<{ onClick={() => { selectEdit(edit.id); selectGeneration(null); - // 设置画布图像为第一个输出资产 - if (edit.outputAssets && edit.outputAssets.length > 0) { + // 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产 + let imageUrl = null; + + // 首先尝试获取参考图像 + if (edit.sourceAssets && edit.sourceAssets.length > 0) { + // 参考图像在uploadResults中从索引outputAssets.length开始 + const outputAssetsCount = edit.outputAssets?.length || 0; + const uploadResultIndex = outputAssetsCount; // 第一个参考图像 + const uploadedUrl = getUploadedImageUrl(edit, uploadResultIndex); + if (uploadedUrl) { + imageUrl = uploadedUrl; + } else if (edit.sourceAssets[0].url) { + imageUrl = edit.sourceAssets[0].url; + } + } + + // 如果没有参考图像,则使用编辑结果图像 + if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) { const asset = edit.outputAssets[0]; if (asset.url) { - setCanvasImage(asset.url); + const uploadedUrl = getUploadedImageUrl(edit, 0); + imageUrl = uploadedUrl || asset.url; } } + + if (imageUrl) { + setCanvasImage(imageUrl); + } }} onMouseEnter={(e) => { // 设置当前悬停的记录 setHoveredRecord({type: 'edit', id: edit.id}); - // 优先使用上传后的远程链接,如果没有则使用原始链接 - let imageUrl = getUploadedImageUrl(edit, 0); - if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) { - imageUrl = edit.outputAssets[0].url; + // 优先显示参考图像,如果没有参考图像则显示编辑结果图像 + let imageUrl = null; + + // 首先尝试获取参考图像 + if (edit.sourceAssets && edit.sourceAssets.length > 0) { + // 参考图像在uploadResults中从索引outputAssets.length开始 + const outputAssetsCount = edit.outputAssets?.length || 0; + const uploadResultIndex = outputAssetsCount; // 第一个参考图像 + imageUrl = getUploadedImageUrl(edit, uploadResultIndex) || + (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) { @@ -889,9 +1016,23 @@ export const HistoryPanel: React.FC<{ }} > {(() => { - // 优先使用上传后的远程链接 - const imageUrl = getUploadedImageUrl(edit, 0) || - (edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null); + // 优先显示参考图像,如果没有参考图像则显示编辑结果图像 + let imageUrl = null; + + // 首先尝试获取参考图像 + if (edit.sourceAssets && edit.sourceAssets.length > 0) { + // 参考图像在uploadResults中从索引outputAssets.length开始 + const outputAssetsCount = edit.outputAssets?.length || 0; + const uploadResultIndex = outputAssetsCount; // 第一个参考图像 + imageUrl = getUploadedImageUrl(edit, uploadResultIndex) || + (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 编辑的变体; @@ -918,12 +1059,34 @@ export const HistoryPanel: React.FC<{ className="h-8 w-8 bg-white/90 hover:bg-white text-gray-700 rounded-full shadow-md" onClick={(e) => { e.stopPropagation(); - // 下载图像 - const imageUrl = getUploadedImageUrl(edit, 0) || - (edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null); + // 下载图像 - 优先下载参考图像,如果没有则下载编辑结果图像 + let imageUrl = null; + + // 首先尝试获取参考图像 + if (edit.sourceAssets && edit.sourceAssets.length > 0) { + // 参考图像在uploadResults中从索引outputAssets.length开始 + const outputAssetsCount = edit.outputAssets?.length || 0; + const uploadResultIndex = outputAssetsCount; // 第一个参考图像 + imageUrl = getUploadedImageUrl(edit, uploadResultIndex) || + (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) { - // 使用Promise来处理异步操作 - fetch(imageUrl) + // 使用fetch获取图像数据并创建Blob URL以确保正确下载 + // 添加更多缓存控制头以绕过CDN缓存 + fetch(imageUrl, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }) .then(response => response.blob()) .then(blob => { const url = URL.createObjectURL(blob); @@ -937,6 +1100,14 @@ export const HistoryPanel: React.FC<{ }) .catch(error => { console.error('下载图像失败:', error); + // 如果fetch失败,回退到直接使用a标签 + const link = document.createElement('a'); + link.href = imageUrl; + link.download = `nano-banana-${Date.now()}.png`; + link.target = '_blank'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); }); } }} @@ -1080,7 +1251,7 @@ export const HistoryPanel: React.FC<{ {gen.outputAssets.length} 个生成结果
- {gen.outputAssets.slice(0, 4).map((asset: any, index: number) => { + {gen.outputAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => { // 获取上传后的远程链接(如果存在) const uploadedUrl = gen.uploadResults && gen.uploadResults[index] && gen.uploadResults[index].success ? `${gen.uploadResults[index].url}?x-oss-process=image/quality,q_30` @@ -1169,20 +1340,14 @@ export const HistoryPanel: React.FC<{ {gen.sourceAssets.length} 个参考图像
- {gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => { - // 获取上传后的远程链接(如果存在) - // 参考图像在uploadResults中从索引outputAssets.length开始 - // 但由于gen可能是轻量级记录,我们需要从dbGenerations中获取完整的记录 - const fullGen = dbGenerations.find(item => item.id === gen.id) || gen; - const outputAssetsCount = fullGen.outputAssets?.length || 0; - - // 确保索引在有效范围内 - const uploadResultIndex = outputAssetsCount + index; - const uploadedUrl = fullGen.uploadResults && fullGen.uploadResults[uploadResultIndex] && fullGen.uploadResults[uploadResultIndex].success - ? `${fullGen.uploadResults[uploadResultIndex].url}?x-oss-process=image/quality,q_30` + {gen.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => { + // 优先使用上传后的远程链接,如果没有则使用asset中的URL + // 参考图像在uploadResults中从索引1开始(图像2字段) + const uploadResultIndex = 1 + index; + const uploadedUrl = gen.uploadResults && gen.uploadResults[uploadResultIndex] && gen.uploadResults[uploadResultIndex].success + ? gen.uploadResults[uploadResultIndex].url : null; - // 如果没有上传的URL,则使用asset中的URL const displayUrl = uploadedUrl || asset.url || asset.blobUrl; // 如果URL是blob:开头但已失效,尝试重新创建 @@ -1320,10 +1485,10 @@ export const HistoryPanel: React.FC<{ {selectedEdit.outputAssets.length} 个编辑结果
- {selectedEdit.outputAssets.slice(0, 4).map((asset: any, index: number) => { + {selectedEdit.outputAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => { // 获取上传后的远程链接(如果存在) const uploadedUrl = selectedEdit.uploadResults && selectedEdit.uploadResults[index] && selectedEdit.uploadResults[index].success - ? `${selectedEdit.uploadResults[index].url}?x-oss-process=image/quality,q_30` + ? selectedEdit.uploadResults[index].url : null; // 如果没有上传的URL,则使用asset中的URL @@ -1401,6 +1566,96 @@ export const HistoryPanel: React.FC<{
)} + {/* 编辑参考图像 */} + {selectedEdit.sourceAssets && selectedEdit.sourceAssets.length > 0 && ( +
+
编辑参考图像
+
+ {selectedEdit.sourceAssets.length} 个参考图像 +
+
+ {selectedEdit.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => { + // 优先使用上传后的远程链接,如果没有则使用asset中的URL + // 参考图像在uploadResults中从索引1开始(图像2字段) + const uploadResultIndex = 1 + index; + const uploadedUrl = selectedEdit.uploadResults && selectedEdit.uploadResults[uploadResultIndex] && selectedEdit.uploadResults[uploadResultIndex].success + ? selectedEdit.uploadResults[uploadResultIndex].url + : null; + + const displayUrl = uploadedUrl || asset.url || asset.blobUrl; + + // 如果URL是blob:开头但已失效,尝试重新创建 + if (displayUrl && displayUrl.startsWith('blob:')) { + // 检查blob是否仍然有效 + const img = new Image(); + img.onerror = () => { + // Blob URL可能已失效,尝试重新创建 + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + // 更新显示 + const imgElement = document.querySelector(`img[src="${displayUrl}"]`); + if (imgElement) { + imgElement.src = newUrl; + } + } + }); + }; + img.src = displayUrl; + } + + return ( +
{ + e.stopPropagation(); + setPreviewModal({ + open: true, + imageUrl: displayUrl, + title: `编辑参考图像 ${index + 1}`, + description: `${asset.width} × ${asset.height}` + }); + }} + > + {displayUrl ? ( + {`编辑参考图像 { + // 如果图像加载失败,尝试重新创建Blob URL + if (displayUrl.startsWith('blob:')) { + import('../store/useAppStore').then((module) => { + const useAppStore = module.useAppStore; + const blob = useAppStore.getState().getBlob(displayUrl); + if (blob) { + const newUrl = URL.createObjectURL(blob); + (e.target as HTMLImageElement).src = newUrl; + } + }); + } + }} + /> + ) : ( +
+ +
+ )} +
+ ); + })} + {selectedEdit.sourceAssets.length > 4 && ( +
+ +{selectedEdit.sourceAssets.length - 4} +
+ )} +
+
+ )} + {/* 原始生成参考 */} {parentGen && (
@@ -1415,18 +1670,14 @@ export const HistoryPanel: React.FC<{ 原始参考图像:
- {parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => { - // 获取上传后的远程链接(如果存在) - // 参考图像在uploadResults中从索引outputAssets.length开始 - const outputAssetsCount = parentGen.outputAssets?.length || 0; - - // 确保索引在有效范围内 - const uploadResultIndex = outputAssetsCount + index; + {parentGen.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => { + // 优先使用上传后的远程链接,如果没有则使用asset中的URL + // 参考图像在uploadResults中从索引1开始(图像2字段) + const uploadResultIndex = 1 + index; const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[uploadResultIndex] && parentGen.uploadResults[uploadResultIndex].success - ? `${parentGen.uploadResults[uploadResultIndex].url}?x-oss-process=image/quality,q_30` + ? parentGen.uploadResults[uploadResultIndex].url : null; - // 如果没有上传的URL,则使用asset中的URL const displayUrl = uploadedUrl || asset.url || asset.blobUrl; // 如果URL是blob:开头但已失效,尝试重新创建 diff --git a/src/components/ImageCanvas.tsx b/src/components/ImageCanvas.tsx index a79d6bd..b835585 100644 --- a/src/components/ImageCanvas.tsx +++ b/src/components/ImageCanvas.tsx @@ -2,25 +2,19 @@ import React, { useRef, useEffect, useState, useCallback } from 'react'; import { Stage, Layer, Image as KonvaImage, Line } from 'react-konva'; import { useAppStore } from '../store/useAppStore'; import { Button } from './ui/Button'; -import { ZoomIn, ZoomOut, RotateCcw, Download, Eye, EyeOff, Eraser } from 'lucide-react'; -import { cn } from '../utils/cn'; +import { ZoomIn, ZoomOut, RotateCcw, Download } from 'lucide-react'; export const ImageCanvas: React.FC = () => { const { canvasImage, canvasZoom, setCanvasZoom, - canvasPan, - setCanvasPan, brushStrokes, addBrushStroke, - clearBrushStrokes, showMasks, - setShowMasks, selectedTool, isGenerating, brushSize, - setBrushSize, showHistory, showPromptPanel } = useAppStore(); @@ -50,7 +44,7 @@ export const ImageCanvas: React.FC = () => { } }, 0); } - }, []); + }, [setCanvasZoom]); // 加载图像 useEffect(() => { @@ -60,7 +54,7 @@ export const ImageCanvas: React.FC = () => { console.log('开始加载图像,URL:', canvasImage); img = new window.Image(); - let isCancelled = false; + const isCancelled = false; img.onload = () => { // 检查是否已取消 @@ -195,7 +189,7 @@ export const ImageCanvas: React.FC = () => { image.src = ''; } }; - }, [canvasImage]); // 只依赖canvasImage,避免其他依赖引起循环 + }, [canvasImage, image, setCanvasZoom, stageSize.height, stageSize.width]); // 添加所有依赖项 // 处理舞台大小调整 useEffect(() => { @@ -212,7 +206,7 @@ export const ImageCanvas: React.FC = () => { updateSize(); window.addEventListener('resize', updateSize); return () => window.removeEventListener('resize', updateSize); - }, []); + }, [showPromptPanel, showHistory]); // 监听面板状态变化以调整画布大小 useEffect(() => { @@ -243,14 +237,13 @@ export const ImageCanvas: React.FC = () => { container.addEventListener('wheel', handleWheel, { passive: false }); return () => container.removeEventListener('wheel', handleWheel); - }, [canvasZoom]); + }, [canvasZoom, handleZoom]); - const handleMouseDown = (e: any) => { + const handleMouseDown = (e: Konva.KonvaEventObject) => { if (selectedTool !== 'mask' || !image) return; setIsDrawing(true); const stage = e.target.getStage(); - const pos = stage.getPointerPosition(); // 使用 Konva 的 getRelativePointerPosition 获取准确坐标 const relativePos = stage.getRelativePointerPosition(); @@ -269,11 +262,10 @@ export const ImageCanvas: React.FC = () => { } }; - const handleMouseMove = (e: any) => { + const handleMouseMove = (e: Konva.KonvaEventObject) => { if (!isDrawing || selectedTool !== 'mask' || !image) return; const stage = e.target.getStage(); - const pos = stage.getPointerPosition(); // 使用 Konva 的 getRelativePointerPosition 获取准确坐标 const relativePos = stage.getRelativePointerPosition(); @@ -351,17 +343,17 @@ export const ImageCanvas: React.FC = () => { // 下载第一个上传结果(通常是生成的图像) 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(); - - // 创建下载链接 + // 使用fetch获取图像数据并创建Blob URL以确保正确下载 + // 添加更多缓存控制头以绕过CDN缓存 + fetch(uploadResult.url, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }) + .then(response => response.blob()) + .then(blob => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; @@ -369,64 +361,21 @@ export const ImageCanvas: React.FC = () => { 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('下载失败,未执行回退方案'); - } - } - })(); + URL.revokeObjectURL(url); + }) + .catch(error => { + console.error('下载图像失败:', error); + // 如果fetch失败,回退到直接使用a标签 + const link = document.createElement('a'); + link.href = uploadResult.url; + link.download = `nano-banana-${Date.now()}.png`; + link.target = '_blank'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }); - // 立即返回,让异步操作在后台进行 + // 立即返回 return; } } @@ -463,14 +412,17 @@ export const ImageCanvas: React.FC = () => { document.body.removeChild(link); } else if (canvasImage.startsWith('blob:')) { // Blob URL格式 - // 使用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(); + // 使用fetch获取图像数据并创建Blob URL以确保正确下载 + // 添加更多缓存控制头以绕过CDN缓存 + fetch(canvasImage, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }) + .then(response => response.blob()) + .then(blob => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; @@ -478,68 +430,32 @@ export const ImageCanvas: React.FC = () => { document.body.appendChild(link); link.click(); document.body.removeChild(link); - // 清理创建的URL - setTimeout(() => URL.revokeObjectURL(url), 100); - } 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('下载失败,未执行回退方案'); - } - } - })(); + URL.revokeObjectURL(url); + }) + .catch(error => { + console.error('下载图像失败:', error); + // 如果fetch失败,回退到直接使用a标签 + 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); + }); } else { // 普通URL格式 - // 使用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(); + // 使用fetch获取图像数据并创建Blob URL以确保正确下载 + // 添加更多缓存控制头以绕过CDN缓存 + fetch(canvasImage, { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }) + .then(response => response.blob()) + .then(blob => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; @@ -547,59 +463,19 @@ export const ImageCanvas: React.FC = () => { document.body.appendChild(link); link.click(); document.body.removeChild(link); - // 清理创建的URL - setTimeout(() => URL.revokeObjectURL(url), 100); - } catch (error) { - console.error('下载图像时出错:', 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 = canvasImage; - } catch (canvasError) { - console.error('Canvas方案也失败了:', canvasError); - console.log('下载失败,未执行回退方案'); - } - } - })(); + URL.revokeObjectURL(url); + }) + .catch(error => { + console.error('下载图像失败:', error); + // 如果fetch失败,回退到直接使用a标签 + 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); + }); } } } @@ -647,7 +523,7 @@ export const ImageCanvas: React.FC = () => { width={stageSize.width} height={stageSize.height} draggable={selectedTool !== 'mask'} - onDragEnd={(e) => { + onDragEnd={() => { // 通过stageRef直接获取和设置位置 const stage = stageRef.current; if (stage) { diff --git a/src/components/PromptComposer.tsx b/src/components/PromptComposer.tsx index 6f0dbd8..96e8281 100644 --- a/src/components/PromptComposer.tsx +++ b/src/components/PromptComposer.tsx @@ -1,13 +1,101 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { Textarea } from './ui/Textarea'; 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, urlToBlob } from '../utils/imageUtils'; +import { urlToBlob } from '../utils/imageUtils'; import { PromptHints } from './PromptHints'; import { PromptSuggestions } from './PromptSuggestions'; import { cn } from '../utils/cn'; +import * as referenceImageService from '../services/referenceImageService'; + +// 图像预览组件 +const ImagePreview: React.FC<{ + image: string; + index: number; + selectedTool: 'generate' | 'edit' | 'mask'; + onRemove: () => void; +}> = ({ image, index, onRemove }) => { + const [imageSrc, setImageSrc] = useState(image); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + // 如果是IndexedDB图像,需要获取实际的Blob数据 + if (image.startsWith('indexeddb://')) { + const imageId = image.replace('indexeddb://', ''); + referenceImageService.getReferenceImage(imageId) + .then(blob => { + if (blob) { + const url = URL.createObjectURL(blob); + setImageSrc(url); + setLoading(false); + } else { + setError(true); + setLoading(false); + } + }) + .catch(err => { + console.error('获取参考图像失败:', err); + setError(true); + setLoading(false); + }); + } else { + // 对于其他类型的URL,直接使用 + setImageSrc(image); + setLoading(false); + } + }, [image]); + + // 清理创建的Blob URL + useEffect(() => { + return () => { + if (imageSrc.startsWith('blob:') && imageSrc !== image) { + URL.revokeObjectURL(imageSrc); + } + }; + }, [imageSrc, image]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+ 图像加载失败 +
+ ); + } + + return ( +
+ {`参考图像 setError(true)} + /> + +
+ 参考 {index + 1} +
+
+ ); +}; export const PromptComposer: React.FC = () => { const { @@ -32,8 +120,7 @@ export const PromptComposer: React.FC = () => { setCanvasImage, showPromptPanel, setShowPromptPanel, - clearBrushStrokes, - addBlob + clearBrushStrokes } = useAppStore(); const { generate, cancelGeneration } = useImageGeneration(); @@ -45,12 +132,27 @@ export const PromptComposer: React.FC = () => { const [isDragOver, setIsDragOver] = useState(false); const fileInputRef = useRef(null); + // 初始化参考图像数据库 + useEffect(() => { + const initDB = async () => { + try { + await referenceImageService.initReferenceImageDB(); + console.log('参考图像数据库初始化成功'); + } catch (error) { + console.error('参考图像数据库初始化失败:', error); + } + }; + + initDB(); + }, []); + const handleGenerate = async () => { if (!currentPrompt.trim()) return; if (selectedTool === 'generate') { // 将上传的图像转换为Blob对象 const referenceImageBlobs: Blob[] = []; + for (const img of uploadedImages) { if (img.startsWith('data:')) { // 从base64数据创建Blob @@ -63,6 +165,23 @@ export const PromptComposer: React.FC = () => { ia[i] = byteString.charCodeAt(i); } referenceImageBlobs.push(new Blob([ab], { type: mimeString })); + } else if (img.startsWith('indexeddb://')) { + // 从IndexedDB获取参考图像 + const imageId = img.replace('indexeddb://', ''); + try { + const blob = await referenceImageService.getReferenceImage(imageId); + if (blob) { + referenceImageBlobs.push(blob); + } else { + console.warn('无法从IndexedDB获取参考图像:', imageId); + // 如果无法获取图像,尝试重新上传 + console.log('尝试重新处理参考图像...'); + } + } catch (error) { + console.warn('无法从IndexedDB获取参考图像:', imageId, error); + // 如果无法获取图像,尝试重新上传 + console.log('尝试重新处理参考图像...'); + } } else if (img.startsWith('blob:')) { // 从Blob URL获取Blob const { getBlob } = useAppStore.getState(); @@ -75,17 +194,9 @@ export const PromptComposer: React.FC = () => { const response = await fetch(img); if (response.ok) { const blob = await response.blob(); - // 重新添加到AppStore - const newUrl = useAppStore.getState().addBlob(blob); referenceImageBlobs.push(blob); - // 更新uploadedImages中的URL - const index = uploadedImages.indexOf(img); - if (index !== -1) { - const newImages = [...uploadedImages]; - newImages[index] = newUrl; - useAppStore.getState().clearUploadedImages(); - newImages.forEach(imageUrl => useAppStore.getState().addUploadedImage(imageUrl)); - } + } else { + console.warn('无法重新获取参考图像:', img); } } catch (error) { console.warn('无法重新获取参考图像:', img, error); @@ -102,9 +213,13 @@ export const PromptComposer: React.FC = () => { } } + // 过滤掉无效的Blob,只保留有效的参考图像 + const validBlobs = referenceImageBlobs.filter(blob => blob.size > 0); + + // 即使没有参考图像也继续生成,因为提示文本是必需的 generate({ prompt: currentPrompt, - referenceImages: referenceImageBlobs.length > 0 ? referenceImageBlobs : undefined, + referenceImages: validBlobs.length > 0 ? validBlobs : undefined, temperature, seed: seed !== null ? seed : undefined }); @@ -116,28 +231,36 @@ export const PromptComposer: React.FC = () => { const handleFileUpload = async (file: File) => { if (file && file.type.startsWith('image/')) { try { - // 直接使用Blob创建URL - const blobUrl = addBlob(file); + // 保存参考图像到IndexedDB + const imageId = await referenceImageService.saveReferenceImage(file); + + // 创建一个特殊的URL来标识这是存储在IndexedDB中的图像 + const imageUrl = `indexeddb://${imageId}`; if (selectedTool === 'generate') { // 添加到参考图像(最多2张) if (uploadedImages.length < 2) { - addUploadedImage(blobUrl); + addUploadedImage(imageUrl); } } else if (selectedTool === 'edit') { // 编辑模式下,添加到单独的编辑参考图像(最多2张) if (editReferenceImages.length < 2) { - addEditReferenceImage(blobUrl); + addEditReferenceImage(imageUrl); } // 如果没有画布图像,则设置为画布图像 if (!canvasImage) { - setCanvasImage(blobUrl); + setCanvasImage(imageUrl); } } else if (selectedTool === 'mask') { - // 遮罩模式下,立即设置为画布图像 - clearUploadedImages(); - addUploadedImage(blobUrl); - setCanvasImage(blobUrl); + // 遮罩模式下,将图像添加为参考图像而不是清除现有图像 + // 只有在没有画布图像时才设置为画布图像 + if (!canvasImage) { + setCanvasImage(imageUrl); + } + // 不清除现有的上传图像,而是将新图像添加为参考图像(如果还有空间) + if (uploadedImages.length < 2) { + addUploadedImage(imageUrl); + } } } catch (error) { console.error('上传图像失败:', error); @@ -171,7 +294,7 @@ export const PromptComposer: React.FC = () => { } }; - const handleClearSession = () => { + const handleClearSession = async () => { setCurrentPrompt(''); clearUploadedImages(); clearEditReferenceImages(); @@ -180,6 +303,14 @@ export const PromptComposer: React.FC = () => { setSeed(null); setTemperature(0.7); setShowClearConfirm(false); + + // 清空IndexedDB中的所有参考图像 + try { + await referenceImageService.clearAllReferenceImages(); + console.log('已清空IndexedDB中的所有参考图像'); + } catch (error) { + console.error('清空IndexedDB中的参考图像失败:', error); + } }; const tools = [ @@ -324,25 +455,13 @@ export const PromptComposer: React.FC = () => { (selectedTool === 'edit' && editReferenceImages.length > 0)) && (
{(selectedTool === 'generate' ? uploadedImages : editReferenceImages).map((image, index) => ( -
- {`参考图像 - -
- 参考 {index + 1} -
-
+ selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)} + /> ))}
)} @@ -423,7 +542,7 @@ export const PromptComposer: React.FC = () => {
{ - setCurrentPrompt(currentPrompt ? `${currentPrompt}, ${word}` : word); + setCurrentPrompt(currentPrompt ? `${currentPrompt};${word}` : word); }} minFrequency={3} showTitle={false} @@ -493,7 +612,7 @@ export const PromptComposer: React.FC = () => {