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..5dd6c40 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'; @@ -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); + } + }); + }; }, []); // 在挂载时设置移动设备默认值 @@ -65,15 +75,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 db55e69..0ca9b39 100644 --- a/src/components/HistoryPanel.tsx +++ b/src/components/HistoryPanel.tsx @@ -4,7 +4,6 @@ import { Button } from './ui/Button'; import { History, Download, Trash2, Image as ImageIcon } 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; @@ -498,6 +506,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) { @@ -540,7 +555,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); @@ -610,7 +626,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); @@ -625,11 +641,70 @@ export const HistoryPanel: React.FC<{ )} onClick={() => { selectGeneration(generation.id); - // 设置画布图像为第一个输出资产 + // 设置画布图像为生成结果图像,而不是参考图像 + let imageUrl = null; + + // 优先使用生成结果图像 if (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 && 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) { + // 检查是否是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); } } }} @@ -637,11 +712,27 @@ export const HistoryPanel: React.FC<{ // 设置当前悬停的记录 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.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; // 第一个参考图像 + imageUrl = getUploadedImageUrl(generation, uploadResultIndex) || + (generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null); + } + if (imageUrl) { // 创建图像对象以获取尺寸 const img = new Image(); @@ -659,16 +750,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; @@ -688,9 +839,26 @@ 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.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; // 第一个参考图像 + imageUrl = getUploadedImageUrl(generation, uploadResultIndex) || + (generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null); + } if (imageUrl) { return 生成的变体; @@ -717,12 +885,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); @@ -736,6 +926,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); }); } }} @@ -778,7 +976,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); @@ -794,11 +992,70 @@ export const HistoryPanel: React.FC<{ onClick={() => { selectEdit(edit.id); selectGeneration(null); - // 设置画布图像为第一个输出资产 + // 设置画布图像为编辑结果图像,而不是参考图像 + let imageUrl = null; + + // 优先使用编辑结果图像 if (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 && 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) { + // 检查是否是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); } } }} @@ -806,10 +1063,25 @@ export const HistoryPanel: React.FC<{ // 设置当前悬停的记录 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.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; // 第一个参考图像 + imageUrl = getUploadedImageUrl(edit, uploadResultIndex) || + (edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null); } if (imageUrl) { @@ -858,9 +1130,26 @@ 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.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; // 第一个参考图像 + imageUrl = getUploadedImageUrl(edit, uploadResultIndex) || + (edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null); + } if (imageUrl) { return 编辑的变体; @@ -887,12 +1176,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); @@ -906,6 +1217,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); }); } }} @@ -1049,13 +1368,36 @@ 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` : 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; + } + }); + } + }} + /> + ) : ( +
+ +
+ )}
); })} @@ -1096,18 +1457,37 @@ 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 uploadedUrl = gen.uploadResults && gen.uploadResults[outputAssetsCount + index] && gen.uploadResults[outputAssetsCount + index].success - ? `${gen.uploadResults[outputAssetsCount + index].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; - const displayUrl = uploadedUrl || 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; + } + }); + } + }} + /> + ) : ( +
+ +
+ )}
); })} @@ -1195,6 +1594,185 @@ export const HistoryPanel: React.FC<{
)} + {/* 编辑结果图像 */} + {selectedEdit.outputAssets && selectedEdit.outputAssets.length > 0 && ( +
+
编辑结果
+
+ {selectedEdit.outputAssets.length} 个编辑结果 +
+
+ {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 + : 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} +
+ )} +
+
+ )} + + {/* 编辑参考图像 */} + {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 && (
@@ -1209,15 +1787,37 @@ 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 uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[outputAssetsCount + index] && parentGen.uploadResults[outputAssetsCount + index].success - ? `${parentGen.uploadResults[outputAssetsCount + index].url}?x-oss-process=image/quality,q_30` + {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 : null; - // 对于Blob URL,我们需要从decodedImages中获取解码后的图像数据 - const displayUrl = uploadedUrl || decodedImages[asset.blobUrl] || asset.blobUrl || 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..f245b63 100644 --- a/src/components/ImageCanvas.tsx +++ b/src/components/ImageCanvas.tsx @@ -2,25 +2,21 @@ 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, + setCanvasZoom, setCanvasPan, brushStrokes, addBrushStroke, - clearBrushStrokes, showMasks, - setShowMasks, selectedTool, isGenerating, brushSize, - setBrushSize, showHistory, showPromptPanel } = useAppStore(); @@ -50,119 +46,198 @@ export const ImageCanvas: React.FC = () => { } }, 0); } - }, []); + }, [setCanvasZoom]); // 加载图像 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(); - let 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); } 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; @@ -170,15 +245,8 @@ export const ImageCanvas: React.FC = () => { // 清理图像源以释放内存 img.src = ''; } - - // 清理之前的图像对象 - if (image) { - image.onload = null; - image.onerror = null; - image.src = ''; - } }; - }, [canvasImage]); // 只依赖canvasImage,避免其他依赖引起循环 + }, [canvasImage, setCanvasZoom, setCanvasPan, stageSize.height, stageSize.width]); // 移除image依赖项 // 处理舞台大小调整 useEffect(() => { @@ -195,7 +263,7 @@ export const ImageCanvas: React.FC = () => { updateSize(); window.addEventListener('resize', updateSize); return () => window.removeEventListener('resize', updateSize); - }, []); + }, [showPromptPanel, showHistory]); // 监听面板状态变化以调整画布大小 useEffect(() => { @@ -226,14 +294,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(); @@ -252,11 +319,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(); @@ -334,17 +400,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; @@ -352,64 +418,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; } } @@ -446,14 +469,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; @@ -461,68 +487,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; @@ -530,59 +520,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); + }); } } } @@ -630,7 +580,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 51e9a83..02a07e4 100644 --- a/src/components/PromptComposer.tsx +++ b/src/components/PromptComposer.tsx @@ -1,13 +1,119 @@ -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; + 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); + + 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 ( +
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); + }} + > + {`参考图像 setError(true)} + /> + +
+ 参考 {index + 1} +
+
+ ); +}; export const PromptComposer: React.FC = () => { const { @@ -23,6 +129,7 @@ export const PromptComposer: React.FC = () => { uploadedImages, addUploadedImage, removeUploadedImage, + reorderUploadedImage, clearUploadedImages, editReferenceImages, addEditReferenceImage, @@ -32,8 +139,7 @@ export const PromptComposer: React.FC = () => { setCanvasImage, showPromptPanel, setShowPromptPanel, - clearBrushStrokes, - addBlob + clearBrushStrokes } = useAppStore(); const { generate, cancelGeneration } = useImageGeneration(); @@ -43,14 +149,30 @@ 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); + // 初始化参考图像数据库 + 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,12 +185,42 @@ 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(); 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(); + referenceImageBlobs.push(blob); + } else { + console.warn('无法重新获取参考图像:', img); + } + } catch (error) { + console.warn('无法重新获取参考图像:', img, error); + } } } else { // 从URL获取Blob @@ -81,9 +233,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 }); @@ -95,28 +251,43 @@ 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}`; + + // 同时创建一个可以直接在画布上显示的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) { - addUploadedImage(blobUrl); + addUploadedImage(imageUrl); } } else if (selectedTool === 'edit') { // 编辑模式下,添加到单独的编辑参考图像(最多2张) if (editReferenceImages.length < 2) { - addEditReferenceImage(blobUrl); + addEditReferenceImage(imageUrl); } - // 如果没有画布图像,则设置为画布图像 + // 如果没有画布图像,则设置为画布图像(使用可以直接显示的URL) if (!canvasImage) { - setCanvasImage(blobUrl); + setCanvasImage(displayUrl); } } else if (selectedTool === 'mask') { - // 遮罩模式下,立即设置为画布图像 - clearUploadedImages(); - addUploadedImage(blobUrl); - setCanvasImage(blobUrl); + // 遮罩模式下,将图像添加为参考图像而不是清除现有图像 + // 只有在没有画布图像时才设置为画布图像(使用可以直接显示的URL) + if (!canvasImage) { + setCanvasImage(displayUrl); + } + // 不清除现有的上传图像,而是将新图像添加为参考图像(如果还有空间) + if (uploadedImages.length < 2) { + addUploadedImage(imageUrl); + } } } catch (error) { console.error('上传图像失败:', error); @@ -150,7 +321,32 @@ export const PromptComposer: React.FC = () => { } }; - const handleClearSession = () => { + // 拖拽排序处理函数 + 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(); clearEditReferenceImages(); @@ -159,6 +355,17 @@ 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); + } + + // 清理所有Blob URL + useAppStore.getState().cleanupAllBlobUrls(); }; const tools = [ @@ -303,25 +510,18 @@ 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)} + 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} + /> ))}
)} @@ -402,7 +602,7 @@ export const PromptComposer: React.FC = () => {
{ - setCurrentPrompt(currentPrompt ? `${currentPrompt}, ${word}` : word); + setCurrentPrompt(currentPrompt ? `${currentPrompt};${word}` : word); }} minFrequency={3} showTitle={false} @@ -472,7 +672,7 @@ export const PromptComposer: React.FC = () => {