diff --git a/src/App.tsx b/src/App.tsx index f95a8a9..eafda7d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,7 @@ const queryClient = new QueryClient({ function AppContent() { useKeyboardShortcuts(); - const { showPromptPanel, setShowPromptPanel, setShowHistory } = useAppStore(); + const { showPromptPanel, setShowPromptPanel, showHistory, setShowHistory } = useAppStore(); const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number} | null>(null); const [previewPosition, setPreviewPosition] = useState<{x: number, y: number} | null>(null); const [isPreviewVisible, setIsPreviewVisible] = useState(false); @@ -95,8 +95,8 @@ function AppContent() {
-
-
+
+
@@ -105,8 +105,8 @@ function AppContent() {
-
-
+
+
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 {};