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 { cn } from '../utils/cn';
|
||||||
import { ImagePreviewModal } from './ImagePreviewModal';
|
import { ImagePreviewModal } from './ImagePreviewModal';
|
||||||
import * as indexedDBService from '../services/indexedDBService';
|
import * as indexedDBService from '../services/indexedDBService';
|
||||||
|
import { useIndexedDBListener } from '../hooks/useIndexedDBListener';
|
||||||
|
|
||||||
export const HistoryPanel: React.FC = () => {
|
export const HistoryPanel: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -37,9 +38,8 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
// 存储从Blob URL解码的图像数据
|
// 存储从Blob URL解码的图像数据
|
||||||
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
|
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 存储从IndexedDB获取的完整记录
|
// 使用自定义hook获取IndexedDB记录
|
||||||
const [dbGenerations, setDbGenerations] = useState<any[]>([]);
|
const { generations: dbGenerations, edits: dbEdits, loading, error, refresh } = useIndexedDBListener();
|
||||||
const [dbEdits, setDbEdits] = useState<any[]>([]);
|
|
||||||
|
|
||||||
// 筛选和搜索状态
|
// 筛选和搜索状态
|
||||||
const [startDate, setStartDate] = useState<string>('');
|
const [startDate, setStartDate] = useState<string>('');
|
||||||
@@ -80,50 +80,39 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [canvasImage]);
|
}, [canvasImage]);
|
||||||
|
|
||||||
// 当组件挂载时,从IndexedDB获取记录
|
// 错误处理显示
|
||||||
useEffect(() => {
|
if (error) {
|
||||||
const loadDBRecords = async () => {
|
return (
|
||||||
try {
|
<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">
|
||||||
await indexedDBService.initDB();
|
<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>
|
||||||
const allGenerations = await indexedDBService.getAllGenerations();
|
</div>
|
||||||
const allEdits = await indexedDBService.getAllEdits();
|
<Button
|
||||||
|
variant="ghost"
|
||||||
setDbGenerations(allGenerations);
|
size="icon"
|
||||||
setDbEdits(allEdits);
|
onClick={() => setShowHistory(!showHistory)}
|
||||||
} catch (err) {
|
className="h-6 w-6"
|
||||||
console.error('从IndexedDB加载记录失败:', err);
|
title="隐藏历史面板"
|
||||||
}
|
>
|
||||||
};
|
×
|
||||||
|
</Button>
|
||||||
loadDBRecords();
|
</div>
|
||||||
}, []);
|
<div className="text-center py-8 text-red-500">
|
||||||
|
<p className="text-sm">加载历史记录时出错: {error}</p>
|
||||||
// 当有新记录添加时,重新加载记录
|
<Button
|
||||||
useEffect(() => {
|
variant="outline"
|
||||||
// 监听store中的记录变化,如果有新记录则重新加载
|
size="sm"
|
||||||
const handleStorageChange = (e: StorageEvent) => {
|
className="mt-2"
|
||||||
if (e.key === 'nano-banana-store') {
|
onClick={refresh}
|
||||||
// 重新加载记录
|
>
|
||||||
const loadDBRecords = async () => {
|
重新加载
|
||||||
try {
|
</Button>
|
||||||
const allGenerations = await indexedDBService.getAllGenerations();
|
</div>
|
||||||
const allEdits = await indexedDBService.getAllEdits();
|
</div>
|
||||||
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) => {
|
const filterRecords = (records: any[], isGeneration: boolean) => {
|
||||||
@@ -233,16 +222,7 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={async () => {
|
onClick={refresh}
|
||||||
try {
|
|
||||||
const allGenerations = await indexedDBService.getAllGenerations();
|
|
||||||
const allEdits = await indexedDBService.getAllEdits();
|
|
||||||
setDbGenerations(allGenerations);
|
|
||||||
setDbEdits(allEdits);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('刷新历史记录失败:', err);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
title="刷新历史记录"
|
title="刷新历史记录"
|
||||||
>
|
>
|
||||||
@@ -601,9 +581,37 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
{gen.sourceAssets && gen.sourceAssets.length > 0 && (
|
{gen.sourceAssets && gen.sourceAssets.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-xs font-medium text-gray-500 mb-2">参考图像</h5>
|
<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} 个参考图像
|
{gen.sourceAssets.length} 个参考图像
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -669,6 +677,42 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
<div className="text-xs text-gray-600">
|
<div className="text-xs text-gray-600">
|
||||||
基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1}
|
基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1}
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,11 +47,22 @@ export const useImageGeneration = () => {
|
|||||||
const accessToken = import.meta.env.VITE_ACCESS_TOKEN || '';
|
const accessToken = import.meta.env.VITE_ACCESS_TOKEN || '';
|
||||||
let uploadResults: any[] | undefined;
|
let uploadResults: any[] | undefined;
|
||||||
|
|
||||||
// 上传生成的图像
|
// 上传生成的图像和参考图像
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
try {
|
try {
|
||||||
|
// 上传生成的图像
|
||||||
const imageUrls = outputAssets.map(asset => asset.url);
|
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);
|
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