You've already forked Nano-Banana-AI-Image-Editor
新增 历史记录中的生成详情显示参考图
This commit is contained in:
@@ -5,6 +5,7 @@ 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 {
|
||||
@@ -37,9 +38,8 @@ export const HistoryPanel: React.FC = () => {
|
||||
// 存储从Blob URL解码的图像数据
|
||||
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
|
||||
|
||||
// 存储从IndexedDB获取的完整记录
|
||||
const [dbGenerations, setDbGenerations] = useState<any[]>([]);
|
||||
const [dbEdits, setDbEdits] = useState<any[]>([]);
|
||||
// 使用自定义hook获取IndexedDB记录
|
||||
const { generations: dbGenerations, edits: dbEdits, loading, error, refresh } = useIndexedDBListener();
|
||||
|
||||
// 筛选和搜索状态
|
||||
const [startDate, setStartDate] = useState<string>('');
|
||||
@@ -80,50 +80,39 @@ export const HistoryPanel: React.FC = () => {
|
||||
}
|
||||
}, [canvasImage]);
|
||||
|
||||
// 当组件挂载时,从IndexedDB获取记录
|
||||
useEffect(() => {
|
||||
const loadDBRecords = async () => {
|
||||
try {
|
||||
// 初始化数据库
|
||||
await indexedDBService.initDB();
|
||||
|
||||
// 获取所有生成记录和编辑记录
|
||||
const allGenerations = await indexedDBService.getAllGenerations();
|
||||
const allEdits = await indexedDBService.getAllEdits();
|
||||
|
||||
setDbGenerations(allGenerations);
|
||||
setDbEdits(allEdits);
|
||||
} catch (err) {
|
||||
console.error('从IndexedDB加载记录失败:', err);
|
||||
// 错误处理显示
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-80 bg-white border-l border-gray-200 p-6 flex flex-col h-full">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<History className="h-5 w-5 text-gray-400" />
|
||||
<h3 className="text-sm font-medium text-gray-300">历史记录和变体</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowHistory(!showHistory)}
|
||||
className="h-6 w-6"
|
||||
title="隐藏历史面板"
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center py-8 text-red-500">
|
||||
<p className="text-sm">加载历史记录时出错: {error}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={refresh}
|
||||
>
|
||||
重新加载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
loadDBRecords();
|
||||
}, []);
|
||||
|
||||
// 当有新记录添加时,重新加载记录
|
||||
useEffect(() => {
|
||||
// 监听store中的记录变化,如果有新记录则重新加载
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'nano-banana-store') {
|
||||
// 重新加载记录
|
||||
const loadDBRecords = async () => {
|
||||
try {
|
||||
const allGenerations = await indexedDBService.getAllGenerations();
|
||||
const allEdits = await indexedDBService.getAllEdits();
|
||||
setDbGenerations(allGenerations);
|
||||
setDbEdits(allEdits);
|
||||
} catch (err) {
|
||||
console.error('从IndexedDB重新加载记录失败:', err);
|
||||
}
|
||||
};
|
||||
loadDBRecords();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
return () => window.removeEventListener('storage', handleStorageChange);
|
||||
}, []);
|
||||
|
||||
// 筛选记录的函数
|
||||
const filterRecords = (records: any[], isGeneration: boolean) => {
|
||||
@@ -233,16 +222,7 @@ export const HistoryPanel: React.FC = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const allGenerations = await indexedDBService.getAllGenerations();
|
||||
const allEdits = await indexedDBService.getAllEdits();
|
||||
setDbGenerations(allGenerations);
|
||||
setDbEdits(allEdits);
|
||||
} catch (err) {
|
||||
console.error('刷新历史记录失败:', err);
|
||||
}
|
||||
}}
|
||||
onClick={refresh}
|
||||
className="h-6 w-6"
|
||||
title="刷新历史记录"
|
||||
>
|
||||
@@ -601,9 +581,37 @@ export const HistoryPanel: React.FC = () => {
|
||||
{gen.sourceAssets && gen.sourceAssets.length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-gray-500 mb-2">参考图像</h5>
|
||||
<div className="text-xs text-gray-600">
|
||||
<div className="text-xs text-gray-600 mb-2">
|
||||
{gen.sourceAssets.length} 个参考图像
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPreviewModal({
|
||||
open: true,
|
||||
imageUrl: asset.url,
|
||||
title: `参考图像 ${index + 1}`,
|
||||
description: `${asset.width} × ${asset.height}`
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={asset.url}
|
||||
alt={`参考图像 ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{gen.sourceAssets.length > 4 && (
|
||||
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
|
||||
+{gen.sourceAssets.length - 4}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -669,6 +677,42 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="text-xs text-gray-600">
|
||||
基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1}
|
||||
</div>
|
||||
{/* 显示原始生成的参考图像 */}
|
||||
{parentGen.sourceAssets && parentGen.sourceAssets.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="text-xs text-gray-600 mb-2">
|
||||
原始参考图像:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPreviewModal({
|
||||
open: true,
|
||||
imageUrl: asset.url,
|
||||
title: `原始参考图像 ${index + 1}`,
|
||||
description: `${asset.width} × ${asset.height}`
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={asset.url}
|
||||
alt={`原始参考图像 ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{parentGen.sourceAssets.length > 4 && (
|
||||
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
|
||||
+{parentGen.sourceAssets.length - 4}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -47,11 +47,22 @@ export const useImageGeneration = () => {
|
||||
const accessToken = import.meta.env.VITE_ACCESS_TOKEN || '';
|
||||
let uploadResults: any[] | undefined;
|
||||
|
||||
// 上传生成的图像
|
||||
// 上传生成的图像和参考图像
|
||||
if (accessToken) {
|
||||
try {
|
||||
// 上传生成的图像
|
||||
const imageUrls = outputAssets.map(asset => asset.url);
|
||||
uploadResults = await uploadImages(imageUrls, accessToken);
|
||||
const outputUploadResults = await uploadImages(imageUrls, accessToken);
|
||||
|
||||
// 上传参考图像(如果存在)
|
||||
let referenceUploadResults: any[] = [];
|
||||
if (request.referenceImages && request.referenceImages.length > 0) {
|
||||
const referenceUrls = request.referenceImages.map(img => `data:image/png;base64,${img}`);
|
||||
referenceUploadResults = await uploadImages(referenceUrls, accessToken);
|
||||
}
|
||||
|
||||
// 合并上传结果
|
||||
uploadResults = [...outputUploadResults, ...referenceUploadResults];
|
||||
|
||||
// 检查上传结果
|
||||
const failedUploads = uploadResults.filter(r => !r.success);
|
||||
|
||||
54
src/hooks/useIndexedDBListener.ts
Normal file
54
src/hooks/useIndexedDBListener.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as indexedDBService from '../services/indexedDBService';
|
||||
|
||||
export const useIndexedDBListener = () => {
|
||||
const [generations, setGenerations] = useState<any[]>([]);
|
||||
const [edits, setEdits] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadRecords = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const allGenerations = await indexedDBService.getAllGenerations();
|
||||
const allEdits = await indexedDBService.getAllEdits();
|
||||
setGenerations(allGenerations);
|
||||
setEdits(allEdits);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('从IndexedDB加载记录失败:', err);
|
||||
setError('加载历史记录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化数据库并加载记录
|
||||
const initAndLoad = async () => {
|
||||
try {
|
||||
await indexedDBService.initDB();
|
||||
await loadRecords();
|
||||
} catch (err) {
|
||||
console.error('初始化IndexedDB失败:', err);
|
||||
setError('初始化数据库失败');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initAndLoad();
|
||||
|
||||
// 设置定时器定期检查新记录
|
||||
const interval = setInterval(() => {
|
||||
loadRecords();
|
||||
}, 3000); // 每3秒检查一次
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const refresh = () => {
|
||||
loadRecords();
|
||||
};
|
||||
|
||||
return { generations, edits, loading, error, refresh };
|
||||
};
|
||||
Reference in New Issue
Block a user