+
diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx
index 63c0caf..c9ea7b3 100644
--- a/src/components/HistoryPanel.tsx
+++ b/src/components/HistoryPanel.tsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useAppStore } from '../store/useAppStore';
import { Button } from './ui/Button';
-import { History, Download, Image as ImageIcon } 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';
@@ -23,7 +23,9 @@ export const HistoryPanel: React.FC<{
showHistory,
setShowHistory,
setCanvasImage,
- selectedTool
+ selectedTool,
+ removeGeneration,
+ removeEdit
} = useAppStore();
const { getBlob } = useAppStore.getState();
@@ -46,6 +48,9 @@ export const HistoryPanel: React.FC<{
// 使用自定义hook获取IndexedDB记录
const { generations: dbGenerations, edits: dbEdits, loading, error, refresh } = useIndexedDBListener();
+ // 跟踪当前悬停的记录
+ const [hoveredRecord, setHoveredRecord] = useState<{type: 'generation' | 'edit', id: string} | null>(null);
+
// 筛选和搜索状态
const [startDate, setStartDate] = useState
(() => {
const today = new Date();
@@ -212,18 +217,47 @@ export const HistoryPanel: React.FC<{
decodeBlobImages();
}, [generations, edits, getBlob, decodedImages]);
+ // 监听鼠标离开窗口事件,确保悬浮预览正确关闭
+ 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 (
-
+
@@ -522,6 +556,9 @@ export const HistoryPanel: React.FC<{
}
}}
onMouseEnter={(e) => {
+ // 设置当前悬停的记录
+ setHoveredRecord({type: 'generation', id: generation.id});
+
// 优先使用上传后的远程链接,如果没有则使用原始链接
let imageUrl = getUploadedImageUrl(generation, 0);
if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) {
@@ -563,6 +600,9 @@ export const HistoryPanel: React.FC<{
// 不需要处理鼠标移动事件,因为预览现在在页面中心显示
}}
onMouseLeave={() => {
+ // 清除当前悬停的记录
+ setHoveredRecord(null);
+
setHoveredImage(null);
if (setPreviewPosition) {
setPreviewPosition(null);
@@ -589,6 +629,47 @@ export const HistoryPanel: React.FC<{
G{globalIndex + 1}
+
+ {/* 悬停时显示的按钮 */}
+ {hoveredRecord && hoveredRecord.type === 'generation' && hoveredRecord.id === generation.id && (
+
+
+
+
+ )}
);
});
@@ -629,6 +710,9 @@ export const HistoryPanel: React.FC<{
}
}}
onMouseEnter={(e) => {
+ // 设置当前悬停的记录
+ setHoveredRecord({type: 'edit', id: edit.id});
+
// 优先使用上传后的远程链接,如果没有则使用原始链接
let imageUrl = getUploadedImageUrl(edit, 0);
if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) {
@@ -671,6 +755,9 @@ export const HistoryPanel: React.FC<{
// 不需要处理鼠标移动事件,因为预览现在在页面中心显示
}}
onMouseLeave={() => {
+ // 清除当前悬停的记录
+ setHoveredRecord(null);
+
setHoveredImage(null);
if (setPreviewPosition) {
setPreviewPosition(null);
@@ -697,6 +784,47 @@ export const HistoryPanel: React.FC<{
E{globalIndex + 1}
+
+ {/* 悬停时显示的按钮 */}
+ {hoveredRecord && hoveredRecord.type === 'edit' && hoveredRecord.id === edit.id && (
+
+
+
+
+ )}
);
});
@@ -818,7 +946,8 @@ export const HistoryPanel: React.FC<{
const uploadedUrl = gen.uploadResults && gen.uploadResults[index + 1] && gen.uploadResults[index + 1].success
? `${gen.uploadResults[index + 1].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 (
-
+
@@ -338,8 +338,8 @@ export const PromptComposer: React.FC = () => {
selectedTool === 'generate'
? '描述您想要创建的内容...'
: '描述您想要的修改...'
- }
- className="min-h-[120px] resize-none text-sm rounded-xl"
+ }
+ className="min-h-[180px] resize-none text-sm rounded-xl"
/>
{/* 常用提示词 */}
diff --git a/src/hooks/useImageGeneration.ts b/src/hooks/useImageGeneration.ts
index 302f2c9..5de3f51 100644
--- a/src/hooks/useImageGeneration.ts
+++ b/src/hooks/useImageGeneration.ts
@@ -171,7 +171,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 b36bdd5..29ee22e 100644
--- a/src/services/uploadService.ts
+++ b/src/services/uploadService.ts
@@ -90,34 +90,13 @@ async function getBlobFromUrl(blobUrl: string): Promise
{
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
+ 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获取图像数据')
- }
+ console.error('从AppStore获取Blob时出错:', error);
+ throw new Error('无法从Blob URL获取图像数据');
}
}
diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts
index b2ec279..7e4de2a 100644
--- a/src/store/useAppStore.ts
+++ b/src/store/useAppStore.ts
@@ -111,6 +111,8 @@ interface AppState {
addGeneration: (generation: Generation) => void;
addEdit: (edit: Edit) => void;
+ removeGeneration: (id: string) => void;
+ removeEdit: (id: string) => void;
selectGeneration: (id: string | null) => void;
selectEdit: (id: string | null) => void;
setShowHistory: (show: boolean) => void;
@@ -259,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,
@@ -269,7 +282,7 @@ export const useAppStore = create()(
blobUrl: asset.url
};
}
- // 对于其他URL类型,创建一个新的Blob URL
+ // 对于其他URL类型,直接使用URL
return {
id: asset.id,
type: asset.type,
@@ -519,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 {};