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 {};