功能性整合

This commit is contained in:
2025-09-19 23:35:05 +08:00
parent 7172b16917
commit 480d8cce46
38 changed files with 5734 additions and 7442 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cn } from './utils/cn';
import { Header } from './components/Header';
@@ -22,7 +22,10 @@ const queryClient = new QueryClient({
function AppContent() {
useKeyboardShortcuts();
const { showPromptPanel, setShowPromptPanel, setShowHistory } = useAppStore();
const { showPromptPanel, setShowPromptPanel, showHistory, setShowHistory } = useAppStore();
const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number} | null>(null);
const [previewPosition, setPreviewPosition] = useState<{x: number, y: number} | null>(null);
const [isPreviewVisible, setIsPreviewVisible] = useState(false);
// 在挂载时初始化IndexedDB并清理base64数据
useEffect(() => {
@@ -72,15 +75,28 @@ function AppContent() {
return () => clearInterval(interval);
}, []);
// 控制预览窗口的显示和隐藏动画
useEffect(() => {
if (hoveredImage) {
// 延迟一小段时间后设置为可见,以触发动画
const timer = setTimeout(() => {
setIsPreviewVisible(true);
}, 10);
return () => clearTimeout(timer);
} else {
setIsPreviewVisible(false);
}
}, [hoveredImage]);
return (
<div className="h-screen bg-gray-50 text-gray-900 flex flex-col font-sans">
<div className="card card-lg rounded-none">
<Header />
</div>
<div className="flex-1 flex overflow-hidden p-4 gap-4">
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out", !showPromptPanel && "w-8")}>
<div className="h-full card card-lg">
<div className="flex-1 flex overflow-hidden p-4 gap-4 relative">
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out overflow-hidden", showPromptPanel ? "w-72" : "w-8")}>
<div className={cn("h-full", showPromptPanel ? "card card-lg" : "")}>
<PromptComposer />
</div>
</div>
@@ -89,12 +105,48 @@ function AppContent() {
<ImageCanvas />
</div>
</div>
<div className="flex-shrink-0">
<div className="h-full card card-lg">
<HistoryPanel />
<div className={cn("flex-shrink-0 transition-all duration-300 ease-in-out overflow-hidden", showHistory ? "w-72" : "w-8")}>
<div className={cn("h-full", showHistory ? "card card-lg" : "")}>
<HistoryPanel setHoveredImage={setHoveredImage} setPreviewPosition={setPreviewPosition} />
</div>
</div>
</div>
{/* 悬浮预览 */}
{hoveredImage && (
<div
className="fixed inset-0 z-[99999] flex items-center justify-center pointer-events-none"
>
<div
className="bg-white rounded-xl shadow-2xl border border-gray-300 overflow-hidden max-w-2xl max-h-[80vh] flex flex-col transition-all duration-200 ease-out"
style={{
transform: isPreviewVisible ? 'scale(1)' : 'scale(0.8)',
opacity: isPreviewVisible ? 1 : 0,
transformOrigin: previewPosition ? `${previewPosition.x}px ${previewPosition.y}px` : 'center'
}}
>
<div className="bg-gray-900 text-white text-sm p-3 truncate font-medium">
{hoveredImage.title}
</div>
<div className="flex-1 flex items-center justify-center p-4">
<img
src={hoveredImage.url}
alt="预览"
className="max-w-full max-h-[60vh] object-contain"
/>
</div>
{/* 图像信息 */}
<div className="p-3 bg-gray-50 border-t border-gray-200 text-sm">
{hoveredImage.width && hoveredImage.height && (
<div className="flex justify-between text-gray-600">
<span>:</span>
<span className="text-gray-800 font-medium">{hoveredImage.width} × {hoveredImage.height}</span>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useAppStore } from '../store/useAppStore';
import { Button } from './ui/Button';
import { History, Download, Image as ImageIcon, Trash2 } from 'lucide-react';
@@ -9,7 +9,10 @@ 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 = () => {
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,
@@ -21,14 +24,15 @@ export const HistoryPanel: React.FC = () => {
setShowHistory,
setCanvasImage,
selectedTool,
deleteGeneration,
deleteEdit,
deleteGenerations,
deleteEdits
removeGeneration,
removeEdit
} = useAppStore();
const { getBlob } = useAppStore.getState();
// 使用自定义hook获取IndexedDB记录
const { generations: dbGenerations, edits: dbEdits, loading, error, refresh } = useIndexedDBListener();
const [previewModal, setPreviewModal] = React.useState<{
open: boolean;
imageUrl: string;
@@ -57,17 +61,19 @@ export const HistoryPanel: React.FC = () => {
// 存储从Blob URL解码的图像数据
const [decodedImages, setDecodedImages] = useState<Record<string, string>>({});
// 使用自定义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<string>(() => {
// 初始化时默认显示今天的记录
const today = new Date();
return today.toISOString().split('T')[0]; // 默认为今天
return today.toISOString().split('T')[0];
});
const [endDate, setEndDate] = useState<string>(() => {
// 初始化时默认显示今天的记录
const today = new Date();
return today.toISOString().split('T')[0]; // 默认为今天
return today.toISOString().split('T')[0];
});
const [searchTerm, setSearchTerm] = useState<string>('');
const [showDatePicker, setShowDatePicker] = useState(false); // 控制日期选择器的显示
@@ -80,12 +86,110 @@ export const HistoryPanel: React.FC = () => {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20; // 减少每页显示的项目数
// 悬浮预览状态
const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number} | null>(null);
const [previewPosition, setPreviewPosition] = useState<{x: number, y: number}>({x: 0, y: 0});
// 获取当前图像尺寸
const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null);
// 当currentProject为空时使用dbGenerations和dbEdits作为备选
const displayGenerations = currentProject ? currentProject.generations : dbGenerations;
const displayEdits = currentProject ? currentProject.edits : dbEdits;
const generations = currentProject?.generations || [];
const edits = currentProject?.edits || [];
// 筛选记录的函数
const filterRecords = useCallback((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;
});
}, [startDate, endDate, searchTerm]);
// 筛选后的记录
const filteredGenerations = useMemo(() => filterRecords(displayGenerations, true), [displayGenerations, filterRecords]);
const filteredEdits = useMemo(() => filterRecords(displayEdits, false), [displayEdits, filterRecords]);
React.useEffect(() => {
if (canvasImage) {
const img = new Image();
img.onload = () => {
setImageDimensions({ width: img.width, height: img.height });
};
img.src = canvasImage;
} else {
setImageDimensions(null);
}
}, [canvasImage]);
// 当项目变化时解码Blob图像
useEffect(() => {
const decodeBlobImages = async () => {
const newDecodedImages: Record<string, string> = {};
// 解码生成记录的输出图像
for (const gen of displayGenerations) {
if (Array.isArray(gen.outputAssetsBlobUrls)) {
for (const blobUrl of gen.outputAssetsBlobUrls) {
if (!decodedImages[blobUrl] && blobUrl.startsWith('blob:')) {
const blob = getBlob(blobUrl);
if (blob) {
// 使用Promise来处理FileReader的异步操作
const dataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
newDecodedImages[blobUrl] = dataUrl;
}
}
}
}
}
// 解码编辑记录的输出图像
for (const edit of displayEdits) {
if (Array.isArray(edit.outputAssetsBlobUrls)) {
for (const blobUrl of edit.outputAssetsBlobUrls) {
if (!decodedImages[blobUrl] && blobUrl.startsWith('blob:')) {
const blob = getBlob(blobUrl);
if (blob) {
// 使用Promise来处理FileReader的异步操作
const dataUrl = await new Promise<string>((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 }));
}
};
// 将异步操作包装在立即执行的函数中
(async () => {
await decodeBlobImages();
})();
}, [displayGenerations, displayEdits, getBlob, decodedImages]);
// 获取上传后的图片链接
const getUploadedImageUrl = (generationOrEdit: any, index: number) => {
@@ -99,22 +203,33 @@ export const HistoryPanel: React.FC = () => {
return null;
};
// 获取当前图像尺寸
const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null);
// 加载和错误状态显示
if (loading) {
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 rounded-full card"
title="隐藏历史面板"
>
×
</Button>
</div>
<div className="text-center py-8 text-gray-500">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-400 mx-auto mb-4"></div>
<p className="text-sm">...</p>
</div>
</div>
);
}
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 (
<div className="w-80 bg-white border-l border-gray-200 p-6 flex flex-col h-full">
@@ -147,86 +262,6 @@ export const HistoryPanel: React.FC = () => {
</div>
);
}
// 筛选记录的函数
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<string, string> = {};
// 解码生成记录的输出图像
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<string>((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<string>((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]);
if (!showHistory) {
return (
@@ -304,7 +339,7 @@ export const HistoryPanel: React.FC = () => {
`${dateRange.from.toLocaleDateString()} - ${dateRange.to.toLocaleDateString()}`
)
) : (
"选择日期范围"
"今天"
)}
</Button>
@@ -475,6 +510,7 @@ export const HistoryPanel: React.FC = () => {
size="sm"
className="text-xs p-1.5 rounded-l-none h-7 border-gray-200 text-gray-600 hover:bg-gray-100 card"
onClick={() => {
// 重置为显示今天的记录
const today = new Date(new Date().setHours(0, 0, 0, 0));
const todayStr = today.toISOString().split('T')[0];
setStartDate(todayStr);
@@ -533,13 +569,21 @@ export const HistoryPanel: React.FC = () => {
{/* 显示生成记录 */}
{(() => {
const sortedGenerations = [...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp);
// 合并所有记录并排序,然后分页
const allRecords = [...sortedGenerations, ...[...filteredEdits].sort((a, b) => b.timestamp - a.timestamp)]
.sort((a, b) => b.timestamp - a.timestamp);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedGenerations = sortedGenerations.slice(startIndex, endIndex);
const paginatedRecords = allRecords.slice(startIndex, endIndex);
// 只显示当前页的生成记录
const paginatedGenerations = paginatedRecords.filter(record =>
sortedGenerations.some(gen => gen.id === record.id)
);
return paginatedGenerations.map((generation, index) => {
// 计算全局索引用于显示编号
const globalIndex = startIndex + index;
const globalIndex = allRecords.findIndex(record => record.id === generation.id);
return (
<div
@@ -561,6 +605,9 @@ export const HistoryPanel: React.FC = () => {
}
}}
onMouseEnter={(e) => {
// 设置当前悬停的记录
setHoveredRecord({type: 'generation', id: generation.id});
// 优先使用上传后的远程链接,如果没有则使用原始链接
let imageUrl = getUploadedImageUrl(generation, 0);
if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) {
@@ -576,49 +623,10 @@ export const HistoryPanel: React.FC = () => {
width: img.width,
height: img.height
});
// 计算预览位置,确保不超出屏幕边界
const previewWidth = 300; // 减小预览窗口大小
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
// 获取HistoryPanel的位置
const historyPanel = document.querySelector('.w-72.bg-white.p-4');
const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 };
// 计算相对于HistoryPanel的位置
let x = e.clientX - panelRect.left + offsetX;
let y = e.clientY - panelRect.top + offsetY;
// 确保预览窗口不会超出右边界
if (x + previewWidth > window.innerWidth) {
x = window.innerWidth - previewWidth - 10;
// 传递鼠标位置信息给App组件
if (setPreviewPosition) {
setPreviewPosition({ x: e.clientX, y: e.clientY });
}
// 确保预览窗口不会超出下边界
if (y + previewHeight > window.innerHeight) {
y = window.innerHeight - previewHeight - 10;
}
// 确保预览窗口不会超出左边界
if (x < 0) {
x = 10;
}
// 确保预览窗口不会超出上边界
if (y < 0) {
y = 10;
}
// 添加额外的安全边界检查
x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
setPreviewPosition({x, y});
};
img.onerror = (error) => {
console.error('图像加载失败:', error);
@@ -629,92 +637,25 @@ export const HistoryPanel: React.FC = () => {
width: 0,
height: 0
});
// 计算预览位置
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
let x = e.clientX + offsetX;
let y = e.clientY + offsetY;
// 确保预览窗口不会超出右边界
if (x + previewWidth > window.innerWidth) {
x = window.innerWidth - previewWidth - 10;
// 传递鼠标位置信息给App组件
if (setPreviewPosition) {
setPreviewPosition({ x: e.clientX, y: e.clientY });
}
// 确保预览窗口不会超出下边界
if (y + previewHeight > window.innerHeight) {
y = window.innerHeight - previewHeight - 10;
}
// 确保预览窗口不会超出左边界
if (x < 0) {
x = 10;
}
// 确保预览窗口不会超出上边界
if (y < 0) {
y = 10;
}
// 添加额外的安全边界检查
x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
setPreviewPosition({x, y});
};
img.src = imageUrl;
}
}}
onMouseMove={(e) => {
// 调整预览位置以避免被遮挡
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
// 获取HistoryPanel的位置
const historyPanel = document.querySelector('.w-72.bg-white.p-4');
const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 };
// 计算相对于HistoryPanel的位置
let x = e.clientX - panelRect.left + offsetX;
let y = e.clientY - panelRect.top + offsetY;
// 确保预览窗口不会超出右边界
if (x + previewWidth > (historyPanel ? historyPanel.clientWidth : window.innerWidth)) {
x = (historyPanel ? historyPanel.clientWidth : window.innerWidth) - previewWidth - 10;
}
// 确保预览窗口不会超出下边界
if (y + previewHeight > (historyPanel ? historyPanel.clientHeight : window.innerHeight)) {
y = (historyPanel ? historyPanel.clientHeight : window.innerHeight) - previewHeight - 10;
}
// 确保预览窗口不会超出左边界
if (x < 0) {
x = 10;
}
// 确保预览窗口不会超出上边界
if (y < 0) {
y = 10;
}
// 添加额外的安全边界检查
const maxWidth = historyPanel ? historyPanel.clientWidth : window.innerWidth;
const maxHeight = historyPanel ? historyPanel.clientHeight : window.innerHeight;
x = Math.max(10, Math.min(x, maxWidth - previewWidth - 10));
y = Math.max(10, Math.min(y, maxHeight - previewHeight - 10));
setPreviewPosition({x, y});
onMouseMove={() => {
// 不需要处理鼠标移动事件,因为预览现在在页面中心显示
}}
onMouseLeave={() => {
// 清除当前悬停的记录
setHoveredRecord(null);
setHoveredImage(null);
if (setPreviewPosition) {
setPreviewPosition(null);
}
}}
>
{(() => {
@@ -738,22 +679,56 @@ export const HistoryPanel: React.FC = () => {
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>
{/* 悬停时显示的按钮 */}
{hoveredRecord && hoveredRecord.type === 'generation' && hoveredRecord.id === generation.id && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-2 opacity-0 hover:opacity-100 transition-opacity duration-200">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 bg-white/90 hover:bg-white text-gray-700 rounded-full shadow-md"
onClick={(e) => {
e.stopPropagation();
// 下载图像
const imageUrl = getUploadedImageUrl(generation, 0) ||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
if (imageUrl) {
// 使用Promise来处理异步操作
fetch(imageUrl)
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `nano-banana-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
})
.catch(error => {
console.error('下载图像失败:', error);
});
}
}}
title="下载图像"
>
<Download className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 bg-red-500/90 hover:bg-red-500 text-white rounded-full shadow-md"
onClick={(e) => {
e.stopPropagation();
// 删除记录
removeGeneration(generation.id);
}}
title="删除记录"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
);
});
@@ -762,16 +737,21 @@ export const HistoryPanel: React.FC = () => {
{/* 显示编辑记录 */}
{(() => {
const sortedEdits = [...filteredEdits].sort((a, b) => b.timestamp - a.timestamp);
// 使用之前计算的相同记录列表以确保编号一致
const allRecords = [...[...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp), ...sortedEdits]
.sort((a, b) => b.timestamp - a.timestamp);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedEdits = sortedEdits.slice(startIndex, endIndex);
const paginatedRecords = allRecords.slice(startIndex, endIndex);
// 计算生成记录的数量,用于编辑记录的编号
const generationCount = [...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp).length;
// 只显示当前页的编辑记录
const paginatedEdits = paginatedRecords.filter(record =>
sortedEdits.some(edit => edit.id === record.id)
);
return paginatedEdits.map((edit, index) => {
// 计算全局索引用于显示编号
const globalIndex = startIndex + index;
const globalIndex = allRecords.findIndex(record => record.id === edit.id);
return (
<div
@@ -794,6 +774,9 @@ export const HistoryPanel: React.FC = () => {
}
}}
onMouseEnter={(e) => {
// 设置当前悬停的记录
setHoveredRecord({type: 'edit', id: edit.id});
// 优先使用上传后的远程链接,如果没有则使用原始链接
let imageUrl = getUploadedImageUrl(edit, 0);
if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) {
@@ -810,42 +793,10 @@ export const HistoryPanel: React.FC = () => {
width: img.width,
height: img.height
});
// 计算预览位置,确保不超出屏幕边界
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
let x = e.clientX + offsetX;
let y = e.clientY + offsetY;
// 确保预览窗口不会超出右边界
if (x + previewWidth > window.innerWidth) {
x = window.innerWidth - previewWidth - 10;
// 传递鼠标位置信息给App组件
if (setPreviewPosition) {
setPreviewPosition({ x: e.clientX, y: e.clientY });
}
// 确保预览窗口不会超出下边界
if (y + previewHeight > window.innerHeight) {
y = window.innerHeight - previewHeight - 10;
}
// 确保预览窗口不会超出左边界
if (x < 0) {
x = 10;
}
// 确保预览窗口不会超出上边界
if (y < 0) {
y = 10;
}
// 添加额外的安全边界检查
x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
setPreviewPosition({x, y});
};
img.onerror = (error) => {
console.error('图像加载失败:', error);
@@ -856,88 +807,25 @@ export const HistoryPanel: React.FC = () => {
width: 0,
height: 0
});
// 计算预览位置
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
// 获取HistoryPanel的位置信息
const historyPanel = e.currentTarget.closest('.w-72');
const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 };
// 计算相对于整个视窗的位置
let x = e.clientX + offsetX;
let y = e.clientY + offsetY;
// 确保预览窗口不会超出右边界
if (x + previewWidth > window.innerWidth) {
x = window.innerWidth - previewWidth - 10;
// 传递鼠标位置信息给App组件
if (setPreviewPosition) {
setPreviewPosition({ x: e.clientX, y: e.clientY });
}
// 确保预览窗口不会超出下边界
if (y + previewHeight > window.innerHeight) {
y = window.innerHeight - previewHeight - 10;
}
// 确保预览窗口不会超出左边界
if (x < 0) {
x = 10;
}
// 确保预览窗口不会超出上边界
if (y < 0) {
y = 10;
}
// 添加额外的安全边界检查
x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
setPreviewPosition({x, y});
};
img.src = imageUrl;
}
}}
onMouseMove={(e) => {
// 调整预览位置以避免被遮挡
const previewWidth = 300;
const previewHeight = 300;
const offsetX = 10;
const offsetY = 10;
let x = e.clientX + offsetX;
let y = e.clientY + offsetY;
// 确保预览窗口不会超出右边界
if (x + previewWidth > window.innerWidth) {
x = window.innerWidth - previewWidth - 10;
}
// 确保预览窗口不会超出下边界
if (y + previewHeight > window.innerHeight) {
y = window.innerHeight - previewHeight - 10;
}
// 确保预览窗口不会超出左边界
if (x < 0) {
x = 10;
}
// 确保预览窗口不会超出上边界
if (y < 0) {
y = 10;
}
// 添加额外的安全边界检查
x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10));
y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10));
setPreviewPosition({x, y});
onMouseMove={() => {
// 不需要处理鼠标移动事件,因为预览现在在页面中心显示
}}
onMouseLeave={() => {
// 清除当前悬停的记录
setHoveredRecord(null);
setHoveredImage(null);
if (setPreviewPosition) {
setPreviewPosition(null);
}
}}
>
{(() => {
@@ -961,22 +849,56 @@ export const HistoryPanel: React.FC = () => {
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>
{/* 悬停时显示的按钮 */}
{hoveredRecord && hoveredRecord.type === 'edit' && hoveredRecord.id === edit.id && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center gap-2 opacity-0 hover:opacity-100 transition-opacity duration-200">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 bg-white/90 hover:bg-white text-gray-700 rounded-full shadow-md"
onClick={(e) => {
e.stopPropagation();
// 下载图像
const imageUrl = getUploadedImageUrl(edit, 0) ||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
if (imageUrl) {
// 使用Promise来处理异步操作
fetch(imageUrl)
.then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `nano-banana-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
})
.catch(error => {
console.error('下载图像失败:', error);
});
}
}}
title="下载图像"
>
<Download className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 bg-red-500/90 hover:bg-red-500 text-white rounded-full shadow-md"
onClick={(e) => {
e.stopPropagation();
// 删除记录
removeEdit(edit.id);
}}
title="删除记录"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
);
});
@@ -987,7 +909,13 @@ export const HistoryPanel: React.FC = () => {
{/* 分页控件 */}
{(() => {
const totalItems = filteredGenerations.length + filteredEdits.length;
// 合并所有记录并排序,然后计算分页
const allRecords = [
...[...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp),
...[...filteredEdits].sort((a, b) => b.timestamp - a.timestamp)
].sort((a, b) => b.timestamp - a.timestamp);
const totalItems = allRecords.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
// 只在有多页时显示分页控件
@@ -1143,7 +1071,7 @@ export const HistoryPanel: React.FC = () => {
// 获取上传后的远程链接(如果存在)
// 参考图像在uploadResults中从索引outputAssets.length开始
// 但由于gen可能是轻量级记录我们需要从dbGenerations中获取完整的记录
const fullGen = dbGenerations.find(g => g.id === gen.id) || gen;
const fullGen = dbGenerations.find(item => item.id === gen.id) || gen;
const outputAssetsCount = fullGen.outputAssets?.length || 0;
const uploadedUrl = gen.uploadResults && gen.uploadResults[outputAssetsCount + index] && gen.uploadResults[outputAssetsCount + index].success
@@ -1185,7 +1113,7 @@ export const HistoryPanel: React.FC = () => {
</div>
);
} else if (selectedEdit) {
const parentGen = filteredGenerations.find(g => g.id === selectedEdit.parentGenerationId) || dbGenerations.find(g => g.id === selectedEdit.parentGenerationId);
const parentGen = filteredGenerations.find(item => item.id === selectedEdit.parentGenerationId) || dbGenerations.find(item => item.id === selectedEdit.parentGenerationId);
return (
<div className="space-y-3 p-3 bg-gray-50 rounded-lg">
<div className="space-y-2.5 text-xs text-gray-700">
@@ -1243,7 +1171,7 @@ export const HistoryPanel: React.FC = () => {
<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">
基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1}
基于: G{dbGenerations.findIndex(item => item.id === parentGen.id) + 1}
</div>
{/* 显示原始生成的参考图像 */}
{parentGen.sourceAssets && parentGen.sourceAssets.length > 0 && (
@@ -1346,24 +1274,19 @@ export const HistoryPanel: React.FC = () => {
onClick={() => {
// 执行删除操作
if (deleteConfirm.type === 'generation') {
deleteConfirm.ids.forEach(id => deleteGeneration(id));
deleteConfirm.ids.forEach(id => removeGeneration(id));
} else if (deleteConfirm.type === 'edit') {
deleteConfirm.ids.forEach(id => deleteEdit(id));
deleteConfirm.ids.forEach(id => removeEdit(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);
}
deleteConfirm.ids.forEach(id => {
// 检查是生成记录还是编辑记录
if (filteredGenerations.some(gen => gen.id === id)) {
removeGeneration(id);
} else if (filteredEdits.some(edit => edit.id === id)) {
removeEdit(id);
}
});
}
// 关闭对话框
@@ -1378,36 +1301,7 @@ export const HistoryPanel: React.FC = () => {
</div>
)}
{/* 悬浮预览 */}
{hoveredImage && (
<div
className="absolute z-[9999] shadow-2xl border border-gray-300 rounded-lg overflow-hidden bg-white backdrop-blur-sm pointer-events-none"
style={{
left: `${previewPosition.x}px`,
top: `${previewPosition.y}px`,
maxWidth: '200px', // 减小最大宽度
maxHeight: '200px'
}}
>
<div className="bg-gray-900 text-white text-xs p-2 truncate font-medium">
{hoveredImage.title}
</div>
<img
src={hoveredImage.url}
alt="预览"
className="w-full h-auto max-h-[150px] object-contain"
/>
{/* 图像信息 */}
<div className="p-2 bg-gray-50 border-t border-gray-200 text-xs">
{hoveredImage.width && hoveredImage.height && (
<div className="flex justify-between text-gray-600">
<span>:</span>
<span className="text-gray-800">{hoveredImage.width} × {hoveredImage.height}</span>
</div>
)}
</div>
</div>
)}
</div>
);
};

View File

@@ -6,6 +6,7 @@ import { useImageGeneration, useImageEditing } from '../hooks/useImageGeneration
import { Upload, Wand2, Edit3, MousePointer, HelpCircle, ChevronDown, ChevronRight, RotateCcw } from 'lucide-react';
import { blobToBase64, urlToBlob } from '../utils/imageUtils';
import { PromptHints } from './PromptHints';
import { PromptSuggestions } from './PromptSuggestions';
import { cn } from '../utils/cn';
export const PromptComposer: React.FC = () => {
@@ -38,6 +39,7 @@ export const PromptComposer: React.FC = () => {
const { generate, cancelGeneration } = useImageGeneration();
const { edit, cancelEdit } = useImageEditing();
const [showAdvanced, setShowAdvanced] = useState(false);
const [showPromptSuggestions, setShowPromptSuggestions] = useState(true);
const [showClearConfirm, setShowClearConfirm] = useState(false);
const [showHintsModal, setShowHintsModal] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
@@ -337,8 +339,8 @@ export const PromptComposer: React.FC = () => {
selectedTool === 'generate'
? '描述您想要创建的内容...'
: '描述您想要的修改...'
}
className="min-h-[120px] resize-none text-sm rounded-xl"
}
className="min-h-[180px] resize-none text-sm rounded-xl"
/>
{/* 提示质量指示器 */}
@@ -386,6 +388,29 @@ export const PromptComposer: React.FC = () => {
)}
</div>
{/* 常用提示词 */}
<div className="pt-3 border-t border-gray-100 flex-shrink-0">
<button
onClick={() => setShowPromptSuggestions(!showPromptSuggestions)}
className="flex items-center text-sm text-gray-500 hover:text-gray-700 transition-colors duration-200"
>
{showPromptSuggestions ? <ChevronDown className="h-4 w-4 mr-1.5" /> : <ChevronRight className="h-4 w-4 mr-1.5" />}
</button>
{showPromptSuggestions && (
<div className="mt-4 animate-in slide-down duration-300">
<PromptSuggestions
onWordSelect={(word) => {
setCurrentPrompt(currentPrompt ? `${currentPrompt}, ${word}` : word);
}}
minFrequency={3}
showTitle={false}
/>
</div>
)}
</div>
{/* 高级控制 */}
<div className="pt-3 border-t border-gray-100 flex-shrink-0">
<button

View File

@@ -0,0 +1,123 @@
import React, { useState, useEffect } from 'react';
import { Button } from './ui/Button';
import { useAppStore } from '../store/useAppStore';
import { cn } from '../utils/cn';
interface WordFrequency {
word: string;
count: number;
}
export const PromptSuggestions: React.FC<{
onWordSelect?: (word: string) => void;
minFrequency?: number;
showTitle?: boolean;
}> = ({ onWordSelect, minFrequency = 3, showTitle = true }) => {
const { currentProject } = useAppStore();
const [frequentWords, setFrequentWords] = useState<WordFrequency[]>([]);
const [showAll, setShowAll] = useState(false);
// 从提示词中提取词语并统计频次
const extractWords = (text: string): string[] => {
// 移除标点符号并分割词语
return text
.toLowerCase()
.replace(/[^\w\s\u4e00-\u9fff]/g, ' ') // 保留中文字符
.split(/\s+/)
.filter(word => word.length > 1); // 过滤掉单字符
};
// 统计词语频次
const calculateWordFrequency = (): WordFrequency[] => {
const wordCount: Record<string, number> = {};
// 收集所有提示词
const allPrompts: string[] = [];
// 添加生成记录的提示词
if (currentProject?.generations) {
currentProject.generations.forEach(gen => {
if (gen.prompt) {
allPrompts.push(gen.prompt);
}
});
}
// 添加编辑记录的指令
if (currentProject?.edits) {
currentProject.edits.forEach(edit => {
if (edit.instruction) {
allPrompts.push(edit.instruction);
}
});
}
// 提取词语并统计频次
allPrompts.forEach(prompt => {
const words = extractWords(prompt);
words.forEach(word => {
wordCount[word] = (wordCount[word] || 0) + 1;
});
});
// 转换为数组并过滤
return Object.entries(wordCount)
.map(([word, count]) => ({ word, count }))
.filter(({ count }) => count >= minFrequency)
.sort((a, b) => b.count - a.count);
};
useEffect(() => {
setFrequentWords(calculateWordFrequency());
}, [currentProject, minFrequency]);
// 显示的词语数量
const displayWords = showAll ? frequentWords : frequentWords.slice(0, 20);
if (frequentWords.length === 0) {
return null;
}
return (
<div className="p-4 bg-gray-50 rounded-lg">
{showTitle && (
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-gray-700"></h3>
{frequentWords.length > 20 && (
<Button
variant="ghost"
size="sm"
className="text-xs text-gray-500 hover:text-gray-700"
onClick={() => setShowAll(!showAll)}
>
{showAll ? '收起' : '展开'}
</Button>
)}
</div>
)}
<div className="flex flex-wrap gap-2">
{displayWords.map(({ word, count }) => (
<button
key={word}
onClick={() => onWordSelect?.(word)}
className={cn(
"px-2 py-1 text-xs rounded-full border transition-all",
"bg-white hover:bg-yellow-50 border-gray-200 hover:border-yellow-300",
"text-gray-700 hover:text-gray-900"
)}
title={`出现频次: ${count}`}
>
{word}
</button>
))}
</div>
{showTitle && frequentWords.length > 20 && (
<div className="mt-2 text-xs text-gray-500 text-center">
{frequentWords.length}
</div>
)}
</div>
);
};

View File

@@ -6,7 +6,6 @@ export const useIndexedDBListener = () => {
const [edits, setEdits] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const isMountedRef = useRef(true);
const loadRecords = async () => {
@@ -55,23 +54,10 @@ export const useIndexedDBListener = () => {
initAndLoad();
// 设置定时器定期检查新记录
intervalRef.current = setInterval(() => {
if (isMountedRef.current) {
loadRecords();
}
}, 3000); // 每3秒检查一次
// 清理函数
return () => {
// 标记组件已卸载
isMountedRef.current = false;
// 清除定时器
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, []);

View File

@@ -109,8 +109,10 @@ interface AppState {
setTemperature: (temp: number) => void;
setSeed: (seed: number | null) => void;
addGeneration: (generation: Generation) => void;
addEdit: (edit: Edit) => void;
addGeneration: (generation) => void;
addEdit: (edit) => void;
removeGeneration: (id: string) => void;
removeEdit: (id: string) => void;
selectGeneration: (id: string | null) => void;
selectEdit: (id: string | null) => void;
setShowHistory: (show: boolean) => void;
@@ -119,12 +121,6 @@ interface AppState {
setSelectedTool: (tool: 'generate' | 'edit' | 'mask') => void;
// 删除历史记录
deleteGeneration: (id: string) => void;
deleteEdit: (id: string) => void;
deleteGenerations: (ids: string[]) => void;
deleteEdits: (ids: string[]) => void;
// Blob存储操作
addBlob: (blob: Blob) => string;
getBlob: (url: string) => Blob | undefined;
@@ -651,239 +647,95 @@ export const useAppStore = create<AppState>()(
});
},
// 删除单个生成记录
deleteGeneration: (id) => set((state) => {
if (!state.currentProject) return {};
// 找到要删除的记录
const generationToDelete = state.currentProject.generations.find(gen => gen.id === id);
if (!generationToDelete) return {};
// 收集需要释放的Blob URLs
const urlsToRevoke: string[] = [];
// 收集生成记录中的Blob URLs
generationToDelete.sourceAssets.forEach(asset => {
if (asset.blobUrl.startsWith('blob:')) {
urlsToRevoke.push(asset.blobUrl);
}
});
generationToDelete.outputAssetsBlobUrls.forEach(url => {
if (url.startsWith('blob:')) {
urlsToRevoke.push(url);
}
});
// 从IndexedDB中删除记录
indexedDBService.deleteGeneration(id).catch(err => {
console.error('从IndexedDB删除生成记录失败:', err);
});
// 释放Blob URLs
if (urlsToRevoke.length > 0) {
set((innerState) => {
urlsToRevoke.forEach(url => {
URL.revokeObjectURL(url);
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.delete(url);
innerState = { ...innerState, blobStore: newBlobStore };
});
return innerState;
});
}
// 如果删除的是当前选中的记录,清除选择
let selectedGenerationId = state.selectedGenerationId;
if (selectedGenerationId === id) {
selectedGenerationId = null;
}
// 更新项目中的生成记录列表
const updatedGenerations = state.currentProject.generations.filter(gen => gen.id !== id);
return {
currentProject: {
...state.currentProject,
generations: updatedGenerations,
updatedAt: Date.now()
},
selectedGenerationId
};
}),
// 删除单个编辑记录
deleteEdit: (id) => set((state) => {
if (!state.currentProject) return {};
// 找到要删除的记录
const editToDelete = state.currentProject.edits.find(edit => edit.id === id);
if (!editToDelete) return {};
// 收集需要释放的Blob URLs
const urlsToRevoke: string[] = [];
// 收集编辑记录中的Blob URLs
if (editToDelete.maskReferenceAssetBlobUrl && editToDelete.maskReferenceAssetBlobUrl.startsWith('blob:')) {
urlsToRevoke.push(editToDelete.maskReferenceAssetBlobUrl);
}
editToDelete.outputAssetsBlobUrls.forEach(url => {
if (url.startsWith('blob:')) {
urlsToRevoke.push(url);
}
});
// 从IndexedDB中删除记录
indexedDBService.deleteEdit(id).catch(err => {
console.error('从IndexedDB删除编辑记录失败:', err);
});
// 释放Blob URLs
if (urlsToRevoke.length > 0) {
set((innerState) => {
urlsToRevoke.forEach(url => {
URL.revokeObjectURL(url);
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.delete(url);
innerState = { ...innerState, blobStore: newBlobStore };
});
return innerState;
});
}
// 如果删除的是当前选中的记录,清除选择
let selectedEditId = state.selectedEditId;
if (selectedEditId === id) {
selectedEditId = null;
}
// 更新项目中的编辑记录列表
const updatedEdits = state.currentProject.edits.filter(edit => edit.id !== id);
return {
currentProject: {
...state.currentProject,
edits: updatedEdits,
updatedAt: Date.now()
},
selectedEditId
};
}),
// 批量删除生成记录
deleteGenerations: (ids) => set((state) => {
// 删除生成记录
removeGeneration: (id) => set((state) => {
if (!state.currentProject) return {};
// 收集需要释放的Blob URLs
const urlsToRevoke: string[] = [];
const generationToRemove = state.currentProject.generations.find(gen => gen.id === id);
// 收集所有要删除记录中的Blob URLs
state.currentProject.generations.forEach(gen => {
if (ids.includes(gen.id)) {
gen.sourceAssets.forEach(asset => {
if (asset.blobUrl.startsWith('blob:')) {
urlsToRevoke.push(asset.blobUrl);
}
});
gen.outputAssetsBlobUrls.forEach(url => {
if (url.startsWith('blob:')) {
urlsToRevoke.push(url);
}
});
}
});
// 从IndexedDB中批量删除记录
indexedDBService.deleteGenerations(ids).catch(err => {
console.error('从IndexedDB批量删除生成记录失败:', err);
});
// 释放Blob URLs
if (urlsToRevoke.length > 0) {
set((innerState) => {
urlsToRevoke.forEach(url => {
URL.revokeObjectURL(url);
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.delete(url);
innerState = { ...innerState, blobStore: newBlobStore };
});
return innerState;
});
}
// 如果删除的是当前选中的记录,清除选择
let selectedGenerationId = state.selectedGenerationId;
if (selectedGenerationId && ids.includes(selectedGenerationId)) {
selectedGenerationId = null;
}
// 更新项目中的生成记录列表
const updatedGenerations = state.currentProject.generations.filter(gen => !ids.includes(gen.id));
return {
currentProject: {
...state.currentProject,
generations: updatedGenerations,
updatedAt: Date.now()
},
selectedGenerationId
};
}),
// 批量删除编辑记录
deleteEdits: (ids) => set((state) => {
if (!state.currentProject) return {};
// 收集需要释放的Blob URLs
const urlsToRevoke: string[] = [];
// 收集所有要删除记录中的Blob URLs
state.currentProject.edits.forEach(edit => {
if (ids.includes(edit.id)) {
if (edit.maskReferenceAssetBlobUrl && edit.maskReferenceAssetBlobUrl.startsWith('blob:')) {
urlsToRevoke.push(edit.maskReferenceAssetBlobUrl);
if (generationToRemove) {
// 收集要删除的生成记录中的Blob URLs
generationToRemove.sourceAssets.forEach(asset => {
if (asset.blobUrl.startsWith('blob:')) {
urlsToRevoke.push(asset.blobUrl);
}
edit.outputAssetsBlobUrls.forEach(url => {
if (url.startsWith('blob:')) {
urlsToRevoke.push(url);
}
});
generationToRemove.outputAssetsBlobUrls.forEach(url => {
if (url.startsWith('blob:')) {
urlsToRevoke.push(url);
}
});
// 释放Blob URLs
if (urlsToRevoke.length > 0) {
set((innerState) => {
urlsToRevoke.forEach(url => {
URL.revokeObjectURL(url);
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.delete(url);
innerState = { ...innerState, blobStore: newBlobStore };
});
return innerState;
});
}
});
// 从IndexedDB中批量删除记录
indexedDBService.deleteEdits(ids).catch(err => {
console.error('从IndexedDB批量删除编辑记录失败:', err);
});
// 释放Blob URLs
if (urlsToRevoke.length > 0) {
set((innerState) => {
urlsToRevoke.forEach(url => {
URL.revokeObjectURL(url);
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.delete(url);
innerState = { ...innerState, blobStore: newBlobStore };
});
return innerState;
});
}
// 如果删除的是当前选中的记录,清除选择
let selectedEditId = state.selectedEditId;
if (selectedEditId && ids.includes(selectedEditId)) {
selectedEditId = null;
}
// 更新项目中的编辑记录列表
const updatedEdits = state.currentProject.edits.filter(edit => !ids.includes(edit.id));
// 从项目中移除生成记录
const updatedProject = {
...state.currentProject,
generations: state.currentProject.generations.filter(gen => gen.id !== id),
updatedAt: Date.now()
};
return {
currentProject: {
...state.currentProject,
edits: updatedEdits,
updatedAt: Date.now()
},
selectedEditId
currentProject: updatedProject
};
}),
// 删除编辑记录
removeEdit: (id) => set((state) => {
if (!state.currentProject) return {};
// 收集需要释放的Blob URLs
const urlsToRevoke: string[] = [];
const editToRemove = state.currentProject.edits.find(edit => edit.id === id);
if (editToRemove) {
// 收集要删除的编辑记录中的Blob URLs
if (editToRemove.maskReferenceAssetBlobUrl && editToRemove.maskReferenceAssetBlobUrl.startsWith('blob:')) {
urlsToRevoke.push(editToRemove.maskReferenceAssetBlobUrl);
}
editToRemove.outputAssetsBlobUrls.forEach(url => {
if (url.startsWith('blob:')) {
urlsToRevoke.push(url);
}
});
// 释放Blob URLs
if (urlsToRevoke.length > 0) {
set((innerState) => {
urlsToRevoke.forEach(url => {
URL.revokeObjectURL(url);
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.delete(url);
innerState = { ...innerState, blobStore: newBlobStore };
});
return innerState;
});
}
}
// 从项目中移除编辑记录
const updatedProject = {
...state.currentProject,
edits: state.currentProject.edits.filter(edit => edit.id !== id),
updatedAt: Date.now()
};
return {
currentProject: updatedProject
};
})
}),