初始化提交

This commit is contained in:
2025-09-19 20:23:07 +08:00
parent c5ee5dd2a3
commit 7172b16917
36 changed files with 7302 additions and 100 deletions

View File

@@ -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