import React, { useState, useEffect } from 'react'; import { useAppStore } from '../store/useAppStore'; import { Button } from './ui/Button'; 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'; import { useIndexedDBListener } from '../hooks/useIndexedDBListener'; import { DayPicker } from 'react-day-picker'; import zhCN from 'react-day-picker/dist/locale/zh-CN'; export const HistoryPanel: React.FC<{ setHoveredImage: (image: {url: string, title: string, width?: number, height?: number} | null) => void, setPreviewPosition?: (position: {x: number, y: number} | null) => void }> = ({ setHoveredImage, setPreviewPosition }) => { const { currentProject, canvasImage, selectedGenerationId, selectedEditId, selectGeneration, selectEdit, showHistory, setShowHistory, setCanvasImage, selectedTool, removeGeneration, removeEdit } = 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 [hoveredRecord, setHoveredRecord] = useState<{type: 'generation' | 'edit', id: string} | null>(null); // 筛选和搜索状态 const [startDate, setStartDate] = useState(() => { const today = new Date(); return today.toISOString().split('T')[0]; // 默认为今天 }); const [endDate, setEndDate] = useState(() => { const today = new Date(); return today.toISOString().split('T')[0]; // 默认为今天 }); const [searchTerm, setSearchTerm] = useState(''); const [showDatePicker, setShowDatePicker] = useState(false); // 控制日期选择器的显示 const [dateRange, setDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({ from: new Date(new Date().setHours(0, 0, 0, 0)), to: new Date(new Date().setHours(0, 0, 0, 0)) }); // 分页状态 const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; // 减少每页显示的项目数 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_30`; // 降低质量到30% } } 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); const recordDateStr = recordDate.toISOString().split('T')[0]; // 获取日期部分 YYYY-MM-DD if (startDate && recordDateStr < startDate) return false; if (endDate && recordDateStr > 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]); // 监听鼠标离开窗口事件,确保悬浮预览正确关闭 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 (
); } return (
{/* 头部 */}

历史记录

{/* 筛选和搜索控件 */}
{showDatePicker && (
{ if (range) { setDateRange(range); // 更新字符串格式的日期用于筛选 if (range.from) { setStartDate(range.from.toISOString().split('T')[0]); } if (range.to) { setEndDate(range.to.toISOString().split('T')[0]); } } }} numberOfMonths={2} className="border-0" locale={zhCN} />
)}
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}/100
{filteredGenerations.length === 0 && filteredEdits.length === 0 ? (
🖼️

暂无历史记录

) : (
{/* 显示生成记录 */} {(() => { const sortedGenerations = [...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const paginatedGenerations = sortedGenerations.slice(startIndex, endIndex); return paginatedGenerations.map((generation, index) => { // 计算全局索引用于显示编号 const globalIndex = startIndex + index; return (
{ selectGeneration(generation.id); // 设置画布图像为第一个输出资产 if (generation.outputAssets && generation.outputAssets.length > 0) { const asset = generation.outputAssets[0]; if (asset.url) { setCanvasImage(asset.url); } } }} onMouseEnter={(e) => { // 设置当前悬停的记录 setHoveredRecord({type: 'generation', id: generation.id}); // 优先使用上传后的远程链接,如果没有则使用原始链接 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 = () => { setHoveredImage({ url: imageUrl, title: `生成记录 G${globalIndex + 1}`, width: img.width, height: img.height }); // 传递鼠标位置信息给App组件 if (setPreviewPosition) { setPreviewPosition({ x: e.clientX, y: e.clientY }); } }; img.onerror = (error) => { console.error('图像加载失败:', error); // 即使图像加载失败,也显示预览 setHoveredImage({ url: imageUrl, title: `生成记录 G${globalIndex + 1}`, width: 0, height: 0 }); // 传递鼠标位置信息给App组件 if (setPreviewPosition) { setPreviewPosition({ x: e.clientX, y: e.clientY }); } }; img.src = imageUrl; } }} onMouseMove={() => { // 不需要处理鼠标移动事件,因为预览现在在页面中心显示 }} onMouseLeave={() => { // 清除当前悬停的记录 setHoveredRecord(null); setHoveredImage(null); if (setPreviewPosition) { setPreviewPosition(null); } }} > {(() => { // 优先使用上传后的远程链接 const imageUrl = getUploadedImageUrl(generation, 0) || (generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null); if (imageUrl) { return 生成的变体; } else { return (
); } })()} {/* 变体编号 */}
G{globalIndex + 1}
{/* 悬停时显示的按钮 */} {hoveredRecord && hoveredRecord.type === 'generation' && hoveredRecord.id === generation.id && (
)}
); }); })()} {/* 显示编辑记录 */} {(() => { const sortedEdits = [...filteredEdits].sort((a, b) => b.timestamp - a.timestamp); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; const paginatedEdits = sortedEdits.slice(startIndex, endIndex); // 计算生成记录的数量,用于编辑记录的编号 const generationCount = [...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp).length; return paginatedEdits.map((edit, index) => { // 计算全局索引用于显示编号 const globalIndex = startIndex + index; return (
{ 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) => { // 设置当前悬停的记录 setHoveredRecord({type: 'edit', id: edit.id}); // 优先使用上传后的远程链接,如果没有则使用原始链接 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 = () => { setHoveredImage({ url: imageUrl, title: `编辑记录 E${globalIndex + 1}`, width: img.width, height: img.height }); // 传递鼠标位置信息给App组件 if (setPreviewPosition) { setPreviewPosition({ x: e.clientX, y: e.clientY }); } }; img.onerror = (error) => { console.error('图像加载失败:', error); // 即使图像加载失败,也显示预览 setHoveredImage({ url: imageUrl, title: `编辑记录 E${globalIndex + 1}`, width: 0, height: 0 }); // 传递鼠标位置信息给App组件 if (setPreviewPosition) { setPreviewPosition({ x: e.clientX, y: e.clientY }); } }; img.src = imageUrl; } }} onMouseMove={() => { // 不需要处理鼠标移动事件,因为预览现在在页面中心显示 }} onMouseLeave={() => { // 清除当前悬停的记录 setHoveredRecord(null); setHoveredImage(null); if (setPreviewPosition) { setPreviewPosition(null); } }} > {(() => { // 优先使用上传后的远程链接 const imageUrl = getUploadedImageUrl(edit, 0) || (edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null); if (imageUrl) { return 编辑的变体; } else { return (
); } })()} {/* 编辑标签 */}
E{globalIndex + 1}
{/* 悬停时显示的按钮 */} {hoveredRecord && hoveredRecord.type === 'edit' && hoveredRecord.id === edit.id && (
)}
); }); })()}
)}
{/* 分页控件 */} {(() => { const totalItems = filteredGenerations.length + filteredEdits.length; const totalPages = Math.ceil(totalItems / itemsPerPage); // 只在有多页时显示分页控件 if (totalPages > 1) { return (
第 {currentPage} 页,共 {totalPages} 页
); } return null; })()} {/* 生成详情 */}

详情

{(() => { 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_30` : null; // 对于Blob URL,我们需要从decodedImages中获取解码后的图像数据 const displayUrl = uploadedUrl || decodedImages[asset.blobUrl] || asset.blobUrl || 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_30` : null; // 对于Blob URL,我们需要从decodedImages中获取解码后的图像数据 const displayUrl = uploadedUrl || decodedImages[asset.blobUrl] || asset.blobUrl || 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} />
); };