diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx index dd2c7e2..db55e69 100644 --- a/src/components/HistoryPanel.tsx +++ b/src/components/HistoryPanel.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useAppStore } from '../store/useAppStore'; import { Button } from './ui/Button'; -import { History, Download, Image as ImageIcon, Trash2 } from 'lucide-react'; +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'; @@ -263,18 +263,47 @@ export const HistoryPanel: React.FC<{ ); } + // 监听鼠标离开窗口事件,确保悬浮预览正确关闭 + useEffect(() => { + const handleMouseLeave = (e: MouseEvent) => { + // 当鼠标离开浏览器窗口时,关闭悬浮预览 + if (e.relatedTarget === null) { + setHoveredImage(null); + if (setPreviewPosition) { + setPreviewPosition(null); + } + } + }; + + const handleBlur = () => { + // 当窗口失去焦点时,关闭悬浮预览 + setHoveredImage(null); + if (setPreviewPosition) { + setPreviewPosition(null); + } + }; + + window.addEventListener('mouseleave', handleMouseLeave); + window.addEventListener('blur', handleBlur); + + return () => { + window.removeEventListener('mouseleave', handleMouseLeave); + window.removeEventListener('blur', handleBlur); + }; + }, [setHoveredImage, setPreviewPosition]); + if (!showHistory) { return ( -
+
@@ -1187,7 +1216,8 @@ export const HistoryPanel: React.FC<{ const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[outputAssetsCount + index] && parentGen.uploadResults[outputAssetsCount + index].success ? `${parentGen.uploadResults[outputAssetsCount + index].url}?x-oss-process=image/quality,q_30` : null; - const displayUrl = uploadedUrl || asset.url; + // 对于Blob URL,我们需要从decodedImages中获取解码后的图像数据 + const displayUrl = uploadedUrl || decodedImages[asset.blobUrl] || asset.blobUrl || asset.url; return (
{ if (!showPromptPanel) { return ( -
+
diff --git a/src/hooks/useImageGeneration.ts b/src/hooks/useImageGeneration.ts index 0411cfe..25a37d2 100644 --- a/src/hooks/useImageGeneration.ts +++ b/src/hooks/useImageGeneration.ts @@ -181,7 +181,7 @@ export const useImageGeneration = () => { id: generateId(), type: 'original' as const, url: blobUrl, // 存储Blob URL而不是base64 - mime: 'image/png', + mime: blob.type || 'image/png', width: 1024, height: 1024, checksum diff --git a/src/services/uploadService.ts b/src/services/uploadService.ts index 451e596..af01728 100644 --- a/src/services/uploadService.ts +++ b/src/services/uploadService.ts @@ -2,31 +2,31 @@ import { UploadResult } from '../types' // 上传接口URL -const UPLOAD_URL = 'https://api.pandorastudio.cn/auth/OSSupload' +const UPLOAD_URL = import.meta.env.VITE_UPLOAD_API // 创建一个Map来缓存已上传的图像 const uploadCache = new Map() // 缓存配置 -const MAX_CACHE_SIZE = 50; // 减少最大缓存条目数 -const CACHE_EXPIRY_TIME = 15 * 60 * 1000; // 缓存过期时间15分钟 +const MAX_CACHE_SIZE = 20 // 减少最大缓存条目数 +const CACHE_EXPIRY_TIME = 15 * 60 * 1000 // 缓存过期时间15分钟 /** * 清理过期的缓存条目 */ function cleanupExpiredCache(): void { - const now = Date.now(); - let deletedCount = 0; - + const now = Date.now() + let deletedCount = 0 + uploadCache.forEach((value, key) => { if (now - value.timestamp > CACHE_EXPIRY_TIME) { - uploadCache.delete(key); - deletedCount++; + uploadCache.delete(key) + deletedCount++ } - }); - + }) + if (deletedCount > 0) { - console.log(`清除了 ${deletedCount} 个过期的缓存条目`); + console.log(`清除了 ${deletedCount} 个过期的缓存条目`) } } @@ -37,16 +37,16 @@ function maintainCacheSize(): void { // 如果缓存大小超过限制,删除最旧的条目 if (uploadCache.size >= MAX_CACHE_SIZE) { // 获取所有条目并按时间排序 - const entries = Array.from(uploadCache.entries()); - entries.sort((a, b) => a[1].timestamp - b[1].timestamp); - + const entries = Array.from(uploadCache.entries()) + entries.sort((a, b) => a[1].timestamp - b[1].timestamp) + // 删除最旧的条目,直到缓存大小在限制内 - const deleteCount = Math.max(1, Math.floor(MAX_CACHE_SIZE * 0.2)); // 删除20%的条目 + const deleteCount = Math.max(1, Math.floor(MAX_CACHE_SIZE * 0.2)) // 删除20%的条目 for (let i = 0; i < deleteCount && uploadCache.size >= MAX_CACHE_SIZE; i++) { - uploadCache.delete(entries[i][0]); + uploadCache.delete(entries[i][0]) } - - console.log(`缓存已满,删除了 ${deleteCount} 个最旧的条目`); + + console.log(`缓存已满,删除了 ${deleteCount} 个最旧的条目`) } } @@ -60,17 +60,22 @@ function getImageHash(imageData: string): string { if (imageData.startsWith('blob:')) { // 对于Blob URL,我们使用URL本身作为标识符的一部分 // 这不是完美的解决方案,但对于大多数情况足够了 - return btoa(imageData).slice(0, 32); + try { + return btoa(imageData).slice(0, 32) + } catch (e) { + // 如果btoa失败(例如包含非Latin1字符),使用encodeURIComponent + return btoa(encodeURIComponent(imageData)).slice(0, 32) + } } - + // 对于base64数据,使用简单的哈希函数生成图像标识符 - let hash = 0; + let hash = 0 for (let i = 0; i < imageData.length; i++) { - const char = imageData.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; // 转换为32位整数 + const char = imageData.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // 转换为32位整数 } - return hash.toString(); + return hash.toString() } /** @@ -81,38 +86,17 @@ function getImageHash(imageData: string): string { async function getBlobFromUrl(blobUrl: string): Promise { try { // 从AppStore获取Blob - const { useAppStore } = await import('../store/useAppStore'); - const blob = useAppStore.getState().getBlob(blobUrl); - + const { useAppStore } = await import('../store/useAppStore') + const blob = useAppStore.getState().getBlob(blobUrl) + if (!blob) { - // 如果AppStore中没有找到Blob,尝试从URL获取 - console.warn('无法从AppStore获取Blob,尝试从URL获取:', blobUrl); - try { - const response = await fetch(blobUrl); - if (!response.ok) { - throw new Error(`获取Blob失败: ${response.status} ${response.statusText}`); - } - return await response.blob(); - } catch (error) { - console.error('从URL获取Blob失败:', error); - throw new Error('无法从Blob URL获取图像数据'); - } + throw new Error('无法从AppStore获取Blob,Blob可能已被清理'); } - + return blob; } catch (error) { console.error('从AppStore获取Blob时出错:', error); - // 如果导入AppStore失败,直接尝试从URL获取 - try { - const response = await fetch(blobUrl); - if (!response.ok) { - throw new Error(`获取Blob失败: ${response.status} ${response.statusText}`); - } - return await response.blob(); - } catch (fetchError) { - console.error('从URL获取Blob失败:', fetchError); - throw new Error('无法从Blob URL获取图像数据'); - } + throw new Error('无法从Blob URL获取图像数据'); } } @@ -125,46 +109,53 @@ async function getBlobFromUrl(blobUrl: string): Promise { */ export const uploadImage = async (imageData: string | Blob, accessToken: string, skipCache: boolean = false): Promise<{ success: boolean; url?: string; error?: string }> => { // 检查缓存中是否已有该图像的上传结果 - const imageHash = typeof imageData === 'string' ? getImageHash(imageData) : 'blob-' + Date.now(); - + const imageHash = typeof imageData === 'string' ? getImageHash(imageData) : 'blob-' + Date.now() + if (!skipCache && typeof imageData === 'string' && uploadCache.has(imageHash)) { - const cachedResult = uploadCache.get(imageHash)!; + const cachedResult = uploadCache.get(imageHash)! // 检查缓存是否过期 if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) { - console.log('从缓存中获取上传结果'); - return cachedResult; + console.log('从缓存中获取上传结果') + // 确保返回的数据结构与新上传的结果一致 + return { + success: cachedResult.success, + url: cachedResult.url, + error: cachedResult.error + } } else { // 缓存过期,删除它 - uploadCache.delete(imageHash); + uploadCache.delete(imageHash) } } try { - let blob: Blob; - + let blob: Blob + if (typeof imageData === 'string') { if (imageData.startsWith('blob:')) { // 从Blob URL获取Blob数据 - blob = await getBlobFromUrl(imageData); + blob = await getBlobFromUrl(imageData) } else if (imageData.includes('base64,')) { // 从base64数据创建Blob - const base64Data = imageData.split('base64,')[1]; - const byteString = atob(base64Data); - const mimeString = 'image/png'; // 默认MIME类型 - const ab = new ArrayBuffer(byteString.length); - const ia = new Uint8Array(ab); + const base64Data = imageData.split('base64,')[1] + const byteString = atob(base64Data) + // 从base64数据中提取MIME类型 + const mimeMatch = imageData.match(/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/) + const mimeString = mimeMatch ? mimeMatch[1] : 'image/png' // 默认MIME类型 + const ab = new ArrayBuffer(byteString.length) + const ia = new Uint8Array(ab) for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); + ia[i] = byteString.charCodeAt(i) } - blob = new Blob([ab], { type: mimeString }); + blob = new Blob([ab], { type: mimeString }) } else { // 从URL获取Blob - const response = await fetch(imageData); - blob = await response.blob(); + const response = await fetch(imageData) + blob = await response.blob() } } else { // 如果已经是Blob对象,直接使用 - blob = imageData; + blob = imageData } // 创建FormData对象,使用唯一文件名 @@ -177,69 +168,70 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string, // 发送POST请求 const response = await fetch(UPLOAD_URL, { method: 'POST', - headers: { - 'accessToken': accessToken, + headers: { + accessToken: accessToken, // 添加其他可能需要的头部 }, body: formData, - }); + }) // 记录响应状态以帮助调试 - console.log('上传响应状态:', response.status, response.statusText); - + console.log('上传响应状态:', response.status, response.statusText) + if (!response.ok) { - const errorText = await response.text(); - console.error('上传失败响应内容:', errorText); - throw new Error(`上传失败: ${response.status} ${response.statusText} - ${errorText}`); + const errorText = await response.text() + console.error('上传失败响应内容:', errorText) + throw new Error(`上传失败: ${response.status} ${response.statusText} - ${errorText}`) } - const result = await response.json(); - console.log('上传响应结果:', result); - + const result = await response.json() + console.log('上传响应结果:', result) + // 根据返回格式处理结果: {"code": 200,"msg": "上传成功","data": "9ecbaa0a0.jpg"} if (result.code === 200) { // 使用环境变量中的VITE_UPLOAD_ASSET_URL作为前缀 - const uploadAssetUrl = import.meta.env.VITE_UPLOAD_ASSET_URL || ''; - const fullUrl = uploadAssetUrl ? `${uploadAssetUrl}/${result.data}` : result.data; - + const uploadAssetUrl = import.meta.env.VITE_UPLOAD_ASSET_URL || '' + const fullUrl = uploadAssetUrl ? `${uploadAssetUrl}/${result.data}` : result.data + // 清理过期缓存 - cleanupExpiredCache(); - + cleanupExpiredCache() + // 维护缓存大小 - maintainCacheSize(); - + maintainCacheSize() + // 将上传结果存储到缓存中 - const uploadResult = { success: true, url: fullUrl, error: undefined }; + const uploadResult = { success: true, url: fullUrl, error: undefined } if (typeof imageData === 'string') { uploadCache.set(imageHash, { ...uploadResult, - timestamp: Date.now() - }); + timestamp: Date.now(), + }) } - - return uploadResult; + + return { success: true, url: fullUrl, error: undefined } } else { - throw new Error(`上传失败: ${result.msg}`); + throw new Error(`上传失败: ${result.msg}`) } } catch (error) { - console.error('上传图像时出错:', error); - const errorResult = { success: false, error: error instanceof Error ? error.message : String(error) }; - + console.error('上传图像时出错:', error) + const errorMessage = error instanceof Error ? error.message : String(error) + const errorResult = { success: false, error: errorMessage } + // 清理过期缓存 - cleanupExpiredCache(); - + cleanupExpiredCache() + // 维护缓存大小(即使是失败的结果也缓存,但时间较短) - maintainCacheSize(); - + maintainCacheSize() + // 将失败的上传结果也存储到缓存中(可选) if (typeof imageData === 'string') { uploadCache.set(imageHash, { ...errorResult, - timestamp: Date.now() - }); + timestamp: Date.now(), + }) } - - return errorResult; + + return { success: false, error: errorMessage } } } @@ -252,43 +244,43 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string, */ export const uploadImages = async (imageDatas: (string | Blob)[], accessToken: string, skipCache: boolean = false): Promise => { try { - const results: UploadResult[] = []; + const results: UploadResult[] = [] for (let i = 0; i < imageDatas.length; i++) { - const imageData = imageDatas[i]; + const imageData = imageDatas[i] try { - const uploadResult = await uploadImage(imageData, accessToken, skipCache); + const uploadResult = await uploadImage(imageData, accessToken, skipCache) const result: UploadResult = { success: uploadResult.success, url: uploadResult.url, error: uploadResult.error, timestamp: Date.now(), - }; - results.push(result); - console.log(`第${i + 1}张图像上传${uploadResult.success ? '成功' : '失败'}:`, uploadResult); + } + results.push(result) + console.log(`第${i + 1}张图像上传${uploadResult.success ? '成功' : '失败'}:`, uploadResult) } catch (error) { const result: UploadResult = { success: false, error: error instanceof Error ? error.message : String(error), timestamp: Date.now(), - }; - results.push(result); - console.error(`第${i + 1}张图像上传失败:`, error); + } + results.push(result) + console.error(`第${i + 1}张图像上传失败:`, error) } } // 检查是否有任何上传失败 - const failedUploads = results.filter(r => !r.success); + const failedUploads = results.filter(r => !r.success) if (failedUploads.length > 0) { - console.warn(`${failedUploads.length}张图像上传失败`); + console.warn(`${failedUploads.length}张图像上传失败`) } else { - console.log(`所有${results.length}张图像上传成功`); + console.log(`所有${results.length}张图像上传成功`) } - return results; + return results } catch (error) { - console.error('批量上传图像时出错:', error); - throw error; + console.error('批量上传图像时出错:', error) + throw error } } @@ -296,6 +288,6 @@ export const uploadImages = async (imageDatas: (string | Blob)[], accessToken: s * 清除上传缓存 */ export const clearUploadCache = (): void => { - uploadCache.clear(); - console.log('上传缓存已清除'); -} \ No newline at end of file + uploadCache.clear() + console.log('上传缓存已清除') +} diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index c635875..b0b226a 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -261,6 +261,17 @@ export const useAppStore = create()( }; } else if (asset.url.startsWith('blob:')) { // 如果已经是Blob URL,直接使用 + // 同时确保存储在blobStore中 + set((innerState) => { + const blob = innerState.blobStore.get(asset.url); + if (blob) { + const newBlobStore = new Map(innerState.blobStore); + newBlobStore.set(asset.url, blob); + return { blobStore: newBlobStore }; + } + return innerState; + }); + return { id: asset.id, type: asset.type, @@ -271,7 +282,7 @@ export const useAppStore = create()( blobUrl: asset.url }; } - // 对于其他URL类型,创建一个新的Blob URL + // 对于其他URL类型,直接使用URL return { id: asset.id, type: asset.type, @@ -521,6 +532,98 @@ export const useAppStore = create()( setSelectedTool: (tool) => set({ selectedTool: tool }), + // 删除生成记录 + removeGeneration: (id) => set((state) => { + if (!state.currentProject) return {}; + + // 收集需要释放的Blob URLs + const urlsToRevoke: string[] = []; + const generationToRemove = state.currentProject.generations.find(gen => gen.id === id); + + if (generationToRemove) { + // 收集要删除的生成记录中的Blob URLs + generationToRemove.sourceAssets.forEach(asset => { + if (asset.blobUrl.startsWith('blob:')) { + urlsToRevoke.push(asset.blobUrl); + } + }); + generationToRemove.outputAssetsBlobUrls.forEach(url => { + if (url.startsWith('blob:')) { + urlsToRevoke.push(url); + } + }); + + // 释放Blob URLs + if (urlsToRevoke.length > 0) { + set((innerState) => { + urlsToRevoke.forEach(url => { + URL.revokeObjectURL(url); + const newBlobStore = new Map(innerState.blobStore); + newBlobStore.delete(url); + innerState = { ...innerState, blobStore: newBlobStore }; + }); + return innerState; + }); + } + } + + // 从项目中移除生成记录 + const updatedProject = { + ...state.currentProject, + generations: state.currentProject.generations.filter(gen => gen.id !== id), + updatedAt: Date.now() + }; + + return { + currentProject: updatedProject + }; + }), + + // 删除编辑记录 + removeEdit: (id) => set((state) => { + if (!state.currentProject) return {}; + + // 收集需要释放的Blob URLs + const urlsToRevoke: string[] = []; + const editToRemove = state.currentProject.edits.find(edit => edit.id === id); + + if (editToRemove) { + // 收集要删除的编辑记录中的Blob URLs + if (editToRemove.maskReferenceAssetBlobUrl && editToRemove.maskReferenceAssetBlobUrl.startsWith('blob:')) { + urlsToRevoke.push(editToRemove.maskReferenceAssetBlobUrl); + } + editToRemove.outputAssetsBlobUrls.forEach(url => { + if (url.startsWith('blob:')) { + urlsToRevoke.push(url); + } + }); + + // 释放Blob URLs + if (urlsToRevoke.length > 0) { + set((innerState) => { + urlsToRevoke.forEach(url => { + URL.revokeObjectURL(url); + const newBlobStore = new Map(innerState.blobStore); + newBlobStore.delete(url); + innerState = { ...innerState, blobStore: newBlobStore }; + }); + return innerState; + }); + } + } + + // 从项目中移除编辑记录 + const updatedProject = { + ...state.currentProject, + edits: state.currentProject.edits.filter(edit => edit.id !== id), + updatedAt: Date.now() + }; + + return { + currentProject: updatedProject + }; + }), + // 清理旧的历史记录 cleanupOldHistory: () => set((state) => { if (!state.currentProject) return {};