You've already forked Nano-Banana-AI-Image-Editor
初始化提交
This commit is contained in:
@@ -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, Image as ImageIcon, Trash2 } from 'lucide-react';
|
||||
import { cn } from '../utils/cn';
|
||||
import { ImagePreviewModal } from './ImagePreviewModal';
|
||||
import * as indexedDBService from '../services/indexedDBService';
|
||||
@@ -20,7 +20,11 @@ export const HistoryPanel: React.FC = () => {
|
||||
showHistory,
|
||||
setShowHistory,
|
||||
setCanvasImage,
|
||||
selectedTool
|
||||
selectedTool,
|
||||
deleteGeneration,
|
||||
deleteEdit,
|
||||
deleteGenerations,
|
||||
deleteEdits
|
||||
} = useAppStore();
|
||||
|
||||
const { getBlob } = useAppStore.getState();
|
||||
@@ -37,6 +41,19 @@ export const HistoryPanel: React.FC = () => {
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 删除确认对话框状态
|
||||
const [deleteConfirm, setDeleteConfirm] = React.useState<{
|
||||
open: boolean;
|
||||
ids: string[];
|
||||
type: 'generation' | 'edit' | 'multiple';
|
||||
count: number;
|
||||
}>({
|
||||
open: false,
|
||||
ids: [],
|
||||
type: 'generation',
|
||||
count: 0
|
||||
});
|
||||
|
||||
// 存储从Blob URL解码的图像数据
|
||||
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -479,9 +496,32 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="mb-6 flex-shrink-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-xs font-medium text-gray-500 uppercase tracking-wide">变体</h4>
|
||||
<span className="text-xs text-gray-400">
|
||||
{filteredGenerations.length + filteredEdits.length}/100
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-gray-400">
|
||||
{filteredGenerations.length + filteredEdits.length}/100
|
||||
</span>
|
||||
{(filteredGenerations.length > 0 || filteredEdits.length > 0) && (
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700 text-xs flex items-center"
|
||||
onClick={() => {
|
||||
const allIds = [
|
||||
...filteredGenerations.map(g => g.id),
|
||||
...filteredEdits.map(e => e.id)
|
||||
];
|
||||
setDeleteConfirm({
|
||||
open: true,
|
||||
ids: allIds,
|
||||
type: 'multiple',
|
||||
count: allIds.length
|
||||
});
|
||||
}}
|
||||
title="清空所有历史记录"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" />
|
||||
清空
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{filteredGenerations.length === 0 && filteredEdits.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
@@ -697,6 +737,23 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="absolute top-1 left-1 bg-gray-900/80 text-xs px-1 py-0.5 rounded text-white">
|
||||
G{globalIndex + 1}
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<button
|
||||
className="absolute top-1 right-1 bg-red-500/80 text-white p-1 rounded-full hover:bg-red-600 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteConfirm({
|
||||
open: true,
|
||||
ids: [generation.id],
|
||||
type: 'generation',
|
||||
count: 1
|
||||
});
|
||||
}}
|
||||
title="删除记录"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -903,6 +960,23 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="absolute top-1 left-1 bg-purple-900/80 text-xs px-1 py-0.5 rounded text-white">
|
||||
E{globalIndex + 1}
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<button
|
||||
className="absolute top-1 right-1 bg-red-500/80 text-white p-1 rounded-full hover:bg-red-600 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeleteConfirm({
|
||||
open: true,
|
||||
ids: [edit.id],
|
||||
type: 'edit',
|
||||
count: 1
|
||||
});
|
||||
}}
|
||||
title="删除记录"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1010,6 +1084,53 @@ export const HistoryPanel: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 生成结果图像 */}
|
||||
{gen.outputAssets && gen.outputAssets.length > 0 && (
|
||||
<div className="pt-2 border-t border-gray-200">
|
||||
<h5 className="text-xs font-medium text-gray-500 mb-2">生成结果</h5>
|
||||
<div className="text-xs text-gray-600 mb-2">
|
||||
{gen.outputAssets.length} 个生成结果
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{gen.outputAssets.slice(0, 4).map((asset: any, index: number) => {
|
||||
// 获取上传后的远程链接(如果存在)
|
||||
const uploadedUrl = gen.uploadResults && gen.uploadResults[index] && gen.uploadResults[index].success
|
||||
? `${gen.uploadResults[index].url}?x-oss-process=image/quality,q_30`
|
||||
: null;
|
||||
|
||||
const displayUrl = uploadedUrl || asset.url;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={asset.id}
|
||||
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer hover:ring-2 hover:ring-yellow-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPreviewModal({
|
||||
open: true,
|
||||
imageUrl: displayUrl,
|
||||
title: `生成结果 ${index + 1}`,
|
||||
description: `${asset.width} × ${asset.height}`
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={displayUrl}
|
||||
alt={`生成结果 ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{gen.outputAssets.length > 4 && (
|
||||
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
|
||||
+{gen.outputAssets.length - 4}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 参考图像信息 */}
|
||||
{gen.sourceAssets && gen.sourceAssets.length > 0 && (
|
||||
<div className="pt-2 border-t border-gray-200">
|
||||
@@ -1020,10 +1141,15 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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`
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
// 但由于gen可能是轻量级记录,我们需要从dbGenerations中获取完整的记录
|
||||
const fullGen = dbGenerations.find(g => g.id === gen.id) || gen;
|
||||
const outputAssetsCount = fullGen.outputAssets?.length || 0;
|
||||
|
||||
const uploadedUrl = gen.uploadResults && gen.uploadResults[outputAssetsCount + index] && gen.uploadResults[outputAssetsCount + index].success
|
||||
? `${gen.uploadResults[outputAssetsCount + index].url}?x-oss-process=image/quality,q_30`
|
||||
: null;
|
||||
|
||||
const displayUrl = uploadedUrl || asset.url;
|
||||
|
||||
return (
|
||||
@@ -1128,9 +1254,10 @@ export const HistoryPanel: React.FC = () => {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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`
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = parentGen.outputAssets?.length || 0;
|
||||
const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[outputAssetsCount + index] && parentGen.uploadResults[outputAssetsCount + index].success
|
||||
? `${parentGen.uploadResults[outputAssetsCount + index].url}?x-oss-process=image/quality,q_30`
|
||||
: null;
|
||||
const displayUrl = uploadedUrl || asset.url;
|
||||
|
||||
@@ -1189,6 +1316,68 @@ export const HistoryPanel: React.FC = () => {
|
||||
description={previewModal.description}
|
||||
/>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
{deleteConfirm.open && (
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center z-[100] backdrop-blur-sm rounded-lg">
|
||||
<div className="bg-white rounded-xl p-6 card-lg max-w-xs w-full mx-4">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
|
||||
<Trash2 className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">确认删除</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
{deleteConfirm.count > 1
|
||||
? `确定要删除这 ${deleteConfirm.count} 条历史记录吗?此操作无法撤销。`
|
||||
: '确定要删除这条历史记录吗?此操作无法撤销。'}
|
||||
</p>
|
||||
<div className="flex justify-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs px-4 py-2 h-8 border-gray-200 text-gray-600 hover:bg-gray-100 card"
|
||||
onClick={() => setDeleteConfirm(prev => ({ ...prev, open: false }))}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="text-xs px-4 py-2 h-8 card"
|
||||
onClick={() => {
|
||||
// 执行删除操作
|
||||
if (deleteConfirm.type === 'generation') {
|
||||
deleteConfirm.ids.forEach(id => deleteGeneration(id));
|
||||
} else if (deleteConfirm.type === 'edit') {
|
||||
deleteConfirm.ids.forEach(id => deleteEdit(id));
|
||||
} else {
|
||||
// 多选删除
|
||||
const genIds = deleteConfirm.ids.filter(id =>
|
||||
filteredGenerations.some(g => g.id === id)
|
||||
);
|
||||
const editIds = deleteConfirm.ids.filter(id =>
|
||||
filteredEdits.some(e => e.id === id)
|
||||
);
|
||||
|
||||
if (genIds.length > 0) {
|
||||
deleteGenerations(genIds);
|
||||
}
|
||||
if (editIds.length > 0) {
|
||||
deleteEdits(editIds);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
setDeleteConfirm(prev => ({ ...prev, open: false }));
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 悬浮预览 */}
|
||||
{hoveredImage && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user