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 { cn } from '../utils/cn'; import { ImagePreviewModal } from './ImagePreviewModal'; import * as indexedDBService from '../services/indexedDBService'; import { useIndexedDBListener } from '../hooks/useIndexedDBListener'; export const HistoryPanel: React.FC = () => { const { currentProject, canvasImage, selectedGenerationId, selectedEditId, selectGeneration, selectEdit, showHistory, setShowHistory, setCanvasImage, selectedTool } = useAppStore(); const { getBlob } = useAppStore.getState(); const [previewModal, setPreviewModal] = React.useState<{ open: boolean; imageUrl: string; title: string; description?: string; }>({ open: false, imageUrl: '', title: '', description: '' }); // 存储从Blob URL解码的图像数据 const [decodedImages, setDecodedImages] = useState>({}); // 使用自定义hook获取IndexedDB记录 const { generations: dbGenerations, edits: dbEdits, loading, error, refresh } = useIndexedDBListener(); // 筛选和搜索状态 const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [searchTerm, setSearchTerm] = useState(''); // 悬浮预览状态 const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number, size?: number} | null>(null); const [previewPosition, setPreviewPosition] = useState<{x: number, y: number}>({x: 0, y: 0}); const generations = currentProject?.generations || []; const edits = currentProject?.edits || []; // 获取上传后的图片链接 const getUploadedImageUrl = (generationOrEdit: any, index: number) => { 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_50`; } } return null; }; // 获取当前图像尺寸 const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null); React.useEffect(() => { if (canvasImage) { const img = new Image(); img.onload = () => { setImageDimensions({ width: img.width, height: img.height }); }; img.src = canvasImage; } else { setImageDimensions(null); } }, [canvasImage]); // 错误处理显示 if (error) { return (

历史记录和变体

加载历史记录时出错: {error}

); } // 筛选记录的函数 const filterRecords = (records: any[], isGeneration: boolean) => { return records.filter(record => { // 日期筛选 const recordDate = new Date(record.timestamp); if (startDate && recordDate < new Date(startDate)) return false; if (endDate && recordDate > new Date(endDate)) return false; // 搜索词筛选 if (searchTerm) { if (isGeneration) { // 生成记录按提示词搜索 return record.prompt.toLowerCase().includes(searchTerm.toLowerCase()); } else { // 编辑记录按指令搜索 return record.instruction.toLowerCase().includes(searchTerm.toLowerCase()); } } return true; }); }; // 筛选后的记录 const filteredGenerations = filterRecords(dbGenerations, true); const filteredEdits = filterRecords(dbEdits, false); // 当项目变化时,解码Blob图像 useEffect(() => { const decodeBlobImages = async () => { const newDecodedImages: Record = {}; // 解码生成记录的输出图像 for (const gen of generations) { if (Array.isArray(gen.outputAssetsBlobUrls)) { for (const blobUrl of gen.outputAssetsBlobUrls) { if (!decodedImages[blobUrl] && blobUrl.startsWith('blob:')) { const blob = getBlob(blobUrl); if (blob) { const dataUrl = await new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.readAsDataURL(blob); }); newDecodedImages[blobUrl] = dataUrl; } } } } } // 解码编辑记录的输出图像 for (const edit of edits) { if (Array.isArray(edit.outputAssetsBlobUrls)) { for (const blobUrl of edit.outputAssetsBlobUrls) { if (!decodedImages[blobUrl] && blobUrl.startsWith('blob:')) { const blob = getBlob(blobUrl); if (blob) { const dataUrl = await new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.readAsDataURL(blob); }); newDecodedImages[blobUrl] = dataUrl; } } } } } if (Object.keys(newDecodedImages).length > 0) { setDecodedImages(prev => ({ ...prev, ...newDecodedImages })); } }; decodeBlobImages(); }, [generations, edits, getBlob, decodedImages]); if (!showHistory) { return (
); } return (
{/* 头部 */}

历史记录

{/* 筛选和搜索控件 */}
setStartDate(e.target.value)} className="flex-1 text-xs p-1.5 border border-gray-200 rounded-lg text-gray-600 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400 card" placeholder="开始日期" /> setEndDate(e.target.value)} className="flex-1 text-xs p-1.5 border border-gray-200 rounded-lg text-gray-600 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400 card" placeholder="结束日期" />
setSearchTerm(e.target.value)} className="flex-1 text-xs p-1.5 border border-gray-200 rounded-l-lg bg-gray-50 focus:outline-none focus:ring-1 focus:ring-yellow-400 card" placeholder="搜索提示词..." />
{/* 变体网格 */}

变体

{filteredGenerations.length + filteredEdits.length}/1000
{filteredGenerations.length === 0 && filteredEdits.length === 0 ? (
🖼️

暂无历史记录

) : (
{/* 显示生成记录 */} {[...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((generation, index) => (
{ selectGeneration(generation.id); // 设置画布图像为第一个输出资产 if (generation.outputAssets && generation.outputAssets.length > 0) { const asset = generation.outputAssets[0]; if (asset.url) { setCanvasImage(asset.url); } } }} onMouseEnter={(e) => { // 优先使用上传后的远程链接,如果没有则使用原始链接 let imageUrl = getUploadedImageUrl(generation, 0); if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) { imageUrl = generation.outputAssets[0].url; } if (imageUrl) { // 创建图像对象以获取尺寸 const img = new Image(); img.onload = () => { // 计算文件大小(仅对base64数据) let size = 0; if (imageUrl.startsWith('data:')) { // 估算base64数据大小 const base64Data = imageUrl.split(',')[1]; size = Math.round((base64Data.length * 3) / 4); } setHoveredImage({ url: imageUrl, title: `生成记录 G${index + 1}`, width: img.width, height: img.height, size: size }); // 计算预览位置,确保不超出屏幕边界 const previewWidth = 300; const previewHeight = 300; const offsetX = 10; const offsetY = 10; // 获取HistoryPanel的位置 const historyPanel = document.querySelector('.w-72.bg-white.p-4'); const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 }; // 计算相对于HistoryPanel的位置 let x = e.clientX - panelRect.left + offsetX; let y = e.clientY - panelRect.top + offsetY; // 确保预览窗口不会超出右边界 if (x + previewWidth > window.innerWidth) { x = window.innerWidth - previewWidth - 10; } // 确保预览窗口不会超出下边界 if (y + previewHeight > window.innerHeight) { y = window.innerHeight - previewHeight - 10; } // 确保预览窗口不会超出左边界 if (x < 0) { x = 10; } // 确保预览窗口不会超出上边界 if (y < 0) { y = 10; } // 添加额外的安全边界检查 x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); setPreviewPosition({x, y}); }; img.onerror = (error) => { console.error('图像加载失败:', error); // 即使图像加载失败,也显示预览 setHoveredImage({ url: imageUrl, title: `生成记录 G${index + 1}`, width: 0, height: 0, size: 0 }); // 计算预览位置 const previewWidth = 300; const previewHeight = 300; const offsetX = 10; const offsetY = 10; let x = e.clientX + offsetX; let y = e.clientY + offsetY; // 确保预览窗口不会超出右边界 if (x + previewWidth > window.innerWidth) { x = window.innerWidth - previewWidth - 10; } // 确保预览窗口不会超出下边界 if (y + previewHeight > window.innerHeight) { y = window.innerHeight - previewHeight - 10; } // 确保预览窗口不会超出左边界 if (x < 0) { x = 10; } // 确保预览窗口不会超出上边界 if (y < 0) { y = 10; } // 添加额外的安全边界检查 x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); setPreviewPosition({x, y}); }; img.src = imageUrl; } }} onMouseMove={(e) => { // 调整预览位置以避免被遮挡 const previewWidth = 300; const previewHeight = 300; const offsetX = 10; const offsetY = 10; // 获取HistoryPanel的位置 const historyPanel = document.querySelector('.w-72.bg-white.p-4'); const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 }; // 计算相对于HistoryPanel的位置 let x = e.clientX - panelRect.left + offsetX; let y = e.clientY - panelRect.top + offsetY; // 确保预览窗口不会超出右边界 if (x + previewWidth > (historyPanel ? historyPanel.clientWidth : window.innerWidth)) { x = (historyPanel ? historyPanel.clientWidth : window.innerWidth) - previewWidth - 10; } // 确保预览窗口不会超出下边界 if (y + previewHeight > (historyPanel ? historyPanel.clientHeight : window.innerHeight)) { y = (historyPanel ? historyPanel.clientHeight : window.innerHeight) - previewHeight - 10; } // 确保预览窗口不会超出左边界 if (x < 0) { x = 10; } // 确保预览窗口不会超出上边界 if (y < 0) { y = 10; } // 添加额外的安全边界检查 const maxWidth = historyPanel ? historyPanel.clientWidth : window.innerWidth; const maxHeight = historyPanel ? historyPanel.clientHeight : window.innerHeight; x = Math.max(10, Math.min(x, maxWidth - previewWidth - 10)); y = Math.max(10, Math.min(y, maxHeight - previewHeight - 10)); setPreviewPosition({x, y}); }} onMouseLeave={() => { setHoveredImage(null); }} > {(() => { // 优先使用上传后的远程链接,如果没有则使用原始链接 const imageUrl = getUploadedImageUrl(generation, 0) || (generation.outputAssets && generation.outputAssets.length > 0 ? generation.outputAssets[0].url : null); if (imageUrl) { return 生成的变体; } else { return (
); } })()} {/* 变体编号 */}
G{index + 1}
))} {/* 显示编辑记录 */} {[...filteredEdits].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((edit, index) => (
{ selectEdit(edit.id); selectGeneration(null); // 设置画布图像为第一个输出资产 if (edit.outputAssets && edit.outputAssets.length > 0) { const asset = edit.outputAssets[0]; if (asset.url) { setCanvasImage(asset.url); } } }} onMouseEnter={(e) => { // 优先使用上传后的远程链接,如果没有则使用原始链接 let imageUrl = getUploadedImageUrl(edit, 0); if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) { imageUrl = edit.outputAssets[0].url; } if (imageUrl) { // 创建图像对象以获取尺寸 const img = new Image(); img.onload = () => { // 计算文件大小(仅对base64数据) let size = 0; if (imageUrl.startsWith('data:')) { // 估算base64数据大小 const base64Data = imageUrl.split(',')[1]; size = Math.round((base64Data.length * 3) / 4); } setHoveredImage({ url: imageUrl, title: `编辑记录 E${index + 1}`, width: img.width, height: img.height, size: size }); // 计算预览位置,确保不超出屏幕边界 const previewWidth = 300; const previewHeight = 300; const offsetX = 10; const offsetY = 10; let x = e.clientX + offsetX; let y = e.clientY + offsetY; // 确保预览窗口不会超出右边界 if (x + previewWidth > window.innerWidth) { x = window.innerWidth - previewWidth - 10; } // 确保预览窗口不会超出下边界 if (y + previewHeight > window.innerHeight) { y = window.innerHeight - previewHeight - 10; } // 确保预览窗口不会超出左边界 if (x < 0) { x = 10; } // 确保预览窗口不会超出上边界 if (y < 0) { y = 10; } // 添加额外的安全边界检查 x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); setPreviewPosition({x, y}); }; img.onerror = (error) => { console.error('图像加载失败:', error); // 即使图像加载失败,也显示预览 setHoveredImage({ url: imageUrl, title: `编辑记录 E${index + 1}`, width: 0, height: 0, size: 0 }); // 计算预览位置 const previewWidth = 300; const previewHeight = 300; const offsetX = 10; const offsetY = 10; // 获取HistoryPanel的位置信息 const historyPanel = e.currentTarget.closest('.w-72'); const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 }; // 计算相对于整个视窗的位置 let x = e.clientX + offsetX; let y = e.clientY + offsetY; // 确保预览窗口不会超出右边界 if (x + previewWidth > window.innerWidth) { x = window.innerWidth - previewWidth - 10; } // 确保预览窗口不会超出下边界 if (y + previewHeight > window.innerHeight) { y = window.innerHeight - previewHeight - 10; } // 确保预览窗口不会超出左边界 if (x < 0) { x = 10; } // 确保预览窗口不会超出上边界 if (y < 0) { y = 10; } // 添加额外的安全边界检查 x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); setPreviewPosition({x, y}); }; img.src = imageUrl; } }} onMouseMove={(e) => { // 调整预览位置以避免被遮挡 const previewWidth = 300; const previewHeight = 300; const offsetX = 10; const offsetY = 10; let x = e.clientX + offsetX; let y = e.clientY + offsetY; // 确保预览窗口不会超出右边界 if (x + previewWidth > window.innerWidth) { x = window.innerWidth - previewWidth - 10; } // 确保预览窗口不会超出下边界 if (y + previewHeight > window.innerHeight) { y = window.innerHeight - previewHeight - 10; } // 确保预览窗口不会超出左边界 if (x < 0) { x = 10; } // 确保预览窗口不会超出上边界 if (y < 0) { y = 10; } // 添加额外的安全边界检查 x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); setPreviewPosition({x, y}); }} onMouseLeave={() => { setHoveredImage(null); }} > {(() => { // 优先使用上传后的远程链接,如果没有则使用原始链接 const imageUrl = getUploadedImageUrl(edit, 0) || (edit.outputAssets && edit.outputAssets.length > 0 ? edit.outputAssets[0].url : null); if (imageUrl) { return 编辑的变体; } else { return (
); } })()} {/* 编辑标签 */}
E{index + 1}
))}
)}
{/* 生成详情 */}

详情

{(() => { const gen = filteredGenerations.find(g => g.id === selectedGenerationId) || dbGenerations.find(g => g.id === selectedGenerationId); const selectedEdit = filteredEdits.find(e => e.id === selectedEditId) || dbEdits.find(e => e.id === selectedEditId); if (gen) { return (
提示:

{gen.prompt}

模型: {gen.modelVersion}
{gen.parameters.seed && (
种子: {gen.parameters.seed}
)}
时间: {new Date(gen.timestamp).toLocaleString()}
{/* 上传结果 */} {gen.uploadResults && gen.uploadResults.length > 0 && (
上传结果
{gen.uploadResults.map((result, index) => (
图像 {index + 1}: {result.success ? '成功' : '失败'}
{result.success && result.url && (
{result.url.split('/').pop()}
)} {result.error && (
{result.error}
)}
))}
)} {/* 参考图像信息 */} {gen.sourceAssets && gen.sourceAssets.length > 0 && (
参考图像
{gen.sourceAssets.length} 个参考图像
{gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => { // 获取上传后的远程链接(如果存在) // 参考图像在uploadResults中从索引1开始(索引0是生成的图像) const uploadedUrl = gen.uploadResults && gen.uploadResults[index + 1] && gen.uploadResults[index + 1].success ? `${gen.uploadResults[index + 1].url}?x-oss-process=image/quality,q_50` : null; const displayUrl = uploadedUrl || asset.url; return (
{ e.stopPropagation(); setPreviewModal({ open: true, imageUrl: displayUrl, title: `参考图像 ${index + 1}`, description: `${asset.width} × ${asset.height}` }); }} > {`参考图像
); })} {gen.sourceAssets.length > 4 && (
+{gen.sourceAssets.length - 4}
)}
)}
); } else if (selectedEdit) { const parentGen = filteredGenerations.find(g => g.id === selectedEdit.parentGenerationId) || dbGenerations.find(g => g.id === selectedEdit.parentGenerationId); return (
编辑指令:

{selectedEdit.instruction}

类型: 图像编辑
创建时间: {new Date(selectedEdit.timestamp).toLocaleString()}
{selectedEdit.maskAssetId && (
遮罩: 已应用
)}
{/* 上传结果 */} {selectedEdit.uploadResults && selectedEdit.uploadResults.length > 0 && (
上传结果
{selectedEdit.uploadResults.map((result, index) => (
图像 {index + 1}: {result.success ? '成功' : '失败'}
{result.success && result.url && (
{result.url.split('/').pop()}
)} {result.error && (
{result.error}
)}
))}
)} {/* 原始生成参考 */} {parentGen && (
原始生成
基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1}
{/* 显示原始生成的参考图像 */} {parentGen.sourceAssets && parentGen.sourceAssets.length > 0 && (
原始参考图像:
{parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => { // 获取上传后的远程链接(如果存在) // 参考图像在uploadResults中从索引1开始(索引0是生成的图像) const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[index + 1] && parentGen.uploadResults[index + 1].success ? `${parentGen.uploadResults[index + 1].url}?x-oss-process=image/quality,q_50` : null; const displayUrl = uploadedUrl || asset.url; return (
{ e.stopPropagation(); setPreviewModal({ open: true, imageUrl: displayUrl, title: `原始参考图像 ${index + 1}`, description: `${asset.width} × ${asset.height}` }); }} > {`原始参考图像
); })} {parentGen.sourceAssets.length > 4 && (
+{parentGen.sourceAssets.length - 4}
)}
)}
)}
); } else { return (

选择一个记录以查看详细信息

); } })()}
{/* 测试按钮 - 用于调试 */}
{/* 操作 */}
{/* 图像预览模态框 */} setPreviewModal(prev => ({ ...prev, open }))} imageUrl={previewModal.imageUrl} title={previewModal.title} description={previewModal.description} /> {/* 悬浮预览 */} {hoveredImage && (
{hoveredImage.title}
预览 {/* 图像信息 */}
{hoveredImage.width && hoveredImage.height && (
尺寸: {hoveredImage.width} × {hoveredImage.height}
)} {hoveredImage.size && (
大小: {Math.round(hoveredImage.size / 1024)} KB
)}
)}
); };