diff --git a/src/App.tsx b/src/App.tsx index 3c8b01e..6af9b66 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { cn } from './utils/cn'; import { Header } from './components/Header'; @@ -8,6 +8,7 @@ import { HistoryPanel } from './components/HistoryPanel'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { useAppStore } from './store/useAppStore'; import { ToastProvider } from './components/ToastContext'; +import * as indexedDBService from './services/indexedDBService'; const queryClient = new QueryClient({ defaultOptions: { @@ -23,6 +24,19 @@ function AppContent() { const { showPromptPanel, setShowPromptPanel, showHistory, setShowHistory } = useAppStore(); + // 在挂载时初始化IndexedDB + useEffect(() => { + const init = async () => { + try { + await indexedDBService.initDB(); + } catch (err) { + console.error('初始化IndexedDB失败:', err); + } + }; + + init(); + }, []); + // 在挂载时设置移动设备默认值 React.useEffect(() => { const checkMobile = () => { diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx index e1046cb..40aa941 100644 --- a/src/components/HistoryPanel.tsx +++ b/src/components/HistoryPanel.tsx @@ -4,6 +4,7 @@ import { Button } from './ui/Button'; import { History, Download, Image as ImageIcon } from 'lucide-react'; import { cn } from '../utils/cn'; import { ImagePreviewModal } from './ImagePreviewModal'; +import * as indexedDBService from '../services/indexedDBService'; export const HistoryPanel: React.FC = () => { const { @@ -36,6 +37,19 @@ export const HistoryPanel: React.FC = () => { // 存储从Blob URL解码的图像数据 const [decodedImages, setDecodedImages] = useState>({}); + // 存储从IndexedDB获取的完整记录 + const [dbGenerations, setDbGenerations] = useState([]); + const [dbEdits, setDbEdits] = useState([]); + + // 筛选和搜索状态 + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + + // 悬浮预览状态 + const [hoveredImage, setHoveredImage] = useState<{url: string, title: string} | null>(null); + const [previewPosition, setPreviewPosition] = useState<{x: number, y: number}>({x: 0, y: 0}); + const generations = currentProject?.generations || []; const edits = currentProject?.edits || []; @@ -66,6 +80,78 @@ 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); + } + }; + + 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) => { + return records.filter(record => { + // 日期筛选 + const recordDate = new Date(record.timestamp); + if (startDate && recordDate < new Date(startDate)) return false; + if (endDate && recordDate > new Date(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 () => { @@ -138,20 +224,81 @@ export const HistoryPanel: React.FC = () => { return (
{/* 头部 */} -
+

历史记录和变体

- +
+ + +
+
+ + {/* 筛选和搜索控件 */} +
+
+ setStartDate(e.target.value)} + className="flex-1 text-xs p-1 border rounded" + placeholder="开始日期" + /> + setEndDate(e.target.value)} + className="flex-1 text-xs p-1 border rounded" + placeholder="结束日期" + /> +
+
+ setSearchTerm(e.target.value)} + className="flex-1 text-xs p-1 border rounded-l" + placeholder="搜索提示词或编辑指令..." + /> + +
{/* 变体网格 */} @@ -159,18 +306,18 @@ export const HistoryPanel: React.FC = () => {

当前变体

- {generations.length + edits.length}/10 + {filteredGenerations.length + filteredEdits.length}/1000
- {generations.length === 0 && edits.length === 0 ? ( + {filteredGenerations.length === 0 && filteredEdits.length === 0 ? (
🖼️

暂无生成记录

) : ( -
+
{/* 显示生成记录 */} - {[...generations].sort((a, b) => b.timestamp - a.timestamp).slice(0, 10).map((generation, index) => ( + {[...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((generation, index) => (
{ onClick={() => { selectGeneration(generation.id); // 设置画布图像为第一个输出资产 - if (generation.outputAssetsBlobUrls && generation.outputAssetsBlobUrls.length > 0) { - const blobUrl = generation.outputAssetsBlobUrls[0]; - const decodedUrl = decodedImages[blobUrl]; - if (decodedUrl) { - setCanvasImage(decodedUrl); - } else if (!blobUrl.startsWith('blob:')) { - // 如果不是Blob URL,直接使用 - setCanvasImage(blobUrl); + if (generation.outputAssets && generation.outputAssets.length > 0) { + const asset = generation.outputAssets[0]; + if (asset.url) { + setCanvasImage(asset.url); } } }} - > - {generation.outputAssetsBlobUrls && generation.outputAssetsBlobUrls.length > 0 ? ( - (() => { - // 首先尝试使用上传后的图片链接 - const uploadedUrl = getUploadedImageUrl(generation, 0); - if (uploadedUrl) { - return 生成的变体; + onMouseEnter={(e) => { + if (generation.outputAssets && generation.outputAssets.length > 0) { + const asset = generation.outputAssets[0]; + if (asset.url) { + setHoveredImage({ + url: asset.url, + title: `生成记录 G${index + 1}: ${generation.prompt.substring(0, 50)}${generation.prompt.length > 50 ? '...' : ''}` + }); + setPreviewPosition({x: e.clientX, y: e.clientY}); } - - // 如果没有上传链接,则使用原来的Blob URL - const blobUrl = generation.outputAssetsBlobUrls[0]; - const decodedUrl = decodedImages[blobUrl]; - if (decodedUrl) { - return 生成的变体; - } else if (!blobUrl.startsWith('blob:')) { - return 生成的变体; + } + }} + 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; + } + + setPreviewPosition({x, y}); + }} + onMouseLeave={() => { + setHoveredImage(null); + }} + > + {generation.outputAssets && generation.outputAssets.length > 0 ? ( + (() => { + const asset = generation.outputAssets[0]; + if (asset.url) { + // 如果是base64数据URL,直接显示 + if (asset.url.startsWith('data:')) { + return 生成的变体; + } + // 如果是普通URL,直接显示 + return 生成的变体; } else { return (
-
+
); } })() ) : (
- +
)} {/* 变体编号 */} -
+
G{index + 1}
))} {/* 显示编辑记录 */} - {[...edits].sort((a, b) => b.timestamp - a.timestamp).slice(0, 10).map((edit, index) => ( + {[...filteredEdits].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((edit, index) => (
{ selectEdit(edit.id); selectGeneration(null); // 设置画布图像为第一个输出资产 - if (edit.outputAssetsBlobUrls && edit.outputAssetsBlobUrls.length > 0) { - const blobUrl = edit.outputAssetsBlobUrls[0]; - const decodedUrl = decodedImages[blobUrl]; - if (decodedUrl) { - setCanvasImage(decodedUrl); - } else if (!blobUrl.startsWith('blob:')) { - // 如果不是Blob URL,直接使用 - setCanvasImage(blobUrl); + if (edit.outputAssets && edit.outputAssets.length > 0) { + const asset = edit.outputAssets[0]; + if (asset.url) { + setCanvasImage(asset.url); } } }} - > - {edit.outputAssetsBlobUrls && edit.outputAssetsBlobUrls.length > 0 ? ( - (() => { - // 首先尝试使用上传后的图片链接 - const uploadedUrl = getUploadedImageUrl(edit, 0); - if (uploadedUrl) { - return 编辑的变体; + onMouseEnter={(e) => { + if (edit.outputAssets && edit.outputAssets.length > 0) { + const asset = edit.outputAssets[0]; + if (asset.url) { + setHoveredImage({ + url: asset.url, + title: `编辑记录 E${index + 1}: ${edit.instruction.substring(0, 50)}${edit.instruction.length > 50 ? '...' : ''}` + }); + setPreviewPosition({x: e.clientX, y: e.clientY}); } - - // 如果没有上传链接,则使用原来的Blob URL - const blobUrl = edit.outputAssetsBlobUrls[0]; - const decodedUrl = decodedImages[blobUrl]; - if (decodedUrl) { - return 编辑的变体; - } else if (!blobUrl.startsWith('blob:')) { - return 编辑的变体; + } + }} + 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; + } + + setPreviewPosition({x, y}); + }} + onMouseLeave={() => { + setHoveredImage(null); + }} + > + {edit.outputAssets && edit.outputAssets.length > 0 ? ( + (() => { + const asset = edit.outputAssets[0]; + if (asset.url) { + // 如果是base64数据URL,直接显示 + if (asset.url.startsWith('data:')) { + return 编辑的变体; + } + // 如果是普通URL,直接显示 + return 编辑的变体; } else { return (
-
+
); } })() ) : (
- +
)} {/* 编辑标签 */} -
+
E{index + 1}
@@ -318,8 +541,8 @@ export const HistoryPanel: React.FC = () => {

生成详情

{(() => { - const gen = generations.find(g => g.id === selectedGenerationId); - const selectedEdit = edits.find(e => e.id === selectedEditId); + const gen = filteredGenerations.find(g => g.id === selectedGenerationId) || dbGenerations.find(g => g.id === selectedGenerationId); + const selectedEdit = filteredEdits.find(e => e.id === selectedEditId) || dbEdits.find(e => e.id === selectedEditId); if (gen) { return ( @@ -375,7 +598,7 @@ export const HistoryPanel: React.FC = () => { )} {/* 参考图像信息 */} - {gen.sourceAssets.length > 0 && ( + {gen.sourceAssets && gen.sourceAssets.length > 0 && (
参考图像
@@ -386,7 +609,7 @@ export const HistoryPanel: React.FC = () => {
); } else if (selectedEdit) { - const parentGen = generations.find(g => g.id === selectedEdit.parentGenerationId); + const parentGen = dbGenerations.find(g => g.id === selectedEdit.parentGenerationId); return (
@@ -444,7 +667,7 @@ export const HistoryPanel: React.FC = () => {
原始生成
- 基于: G{generations.length - generations.indexOf(parentGen)} + 基于: G{dbGenerations.findIndex(g => g.id === parentGen.id) + 1}
)} @@ -520,6 +743,28 @@ export const HistoryPanel: React.FC = () => { title={previewModal.title} description={previewModal.description} /> + + {/* 悬浮预览 */} + {hoveredImage && ( +
+
+ {hoveredImage.title} +
+ 预览 +
+ )}
); }; \ No newline at end of file diff --git a/src/services/indexedDBService.ts b/src/services/indexedDBService.ts new file mode 100644 index 0000000..04ff5e0 --- /dev/null +++ b/src/services/indexedDBService.ts @@ -0,0 +1,208 @@ +import { Generation, Edit } from '../types'; + +// 数据库配置 +const DB_NAME = 'NanoBananaDB'; +const DB_VERSION = 1; +const GENERATIONS_STORE = 'generations'; +const EDITS_STORE = 'edits'; + +// IndexedDB实例 +let db: IDBDatabase | null = null; + +/** + * 初始化数据库 + */ +export const initDB = (): Promise => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => { + console.error('数据库打开失败:', request.error); + reject(request.error); + }; + + request.onsuccess = () => { + db = request.result; + resolve(); + }; + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + + // 创建生成记录存储 + if (!db.objectStoreNames.contains(GENERATIONS_STORE)) { + const genStore = db.createObjectStore(GENERATIONS_STORE, { keyPath: 'id' }); + genStore.createIndex('timestamp', 'timestamp', { unique: false }); + } + + // 创建编辑记录存储 + if (!db.objectStoreNames.contains(EDITS_STORE)) { + const editStore = db.createObjectStore(EDITS_STORE, { keyPath: 'id' }); + editStore.createIndex('timestamp', 'timestamp', { unique: false }); + editStore.createIndex('parentGenerationId', 'parentGenerationId', { unique: false }); + } + }; + }); +}; + +/** + * 获取数据库实例 + */ +const getDB = (): IDBDatabase => { + if (!db) { + throw new Error('数据库未初始化'); + } + return db; +}; + +/** + * 添加生成记录 + */ +export const addGeneration = async (generation: Generation): Promise => { + const db = getDB(); + const transaction = db.transaction([GENERATIONS_STORE], 'readwrite'); + const store = transaction.objectStore(GENERATIONS_STORE); + + return new Promise((resolve, reject) => { + const request = store.add(generation); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +}; + +/** + * 添加编辑记录 + */ +export const addEdit = async (edit: Edit): Promise => { + const db = getDB(); + const transaction = db.transaction([EDITS_STORE], 'readwrite'); + const store = transaction.objectStore(EDITS_STORE); + + return new Promise((resolve, reject) => { + const request = store.add(edit); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +}; + +/** + * 获取所有生成记录(按时间倒序) + */ +export const getAllGenerations = async (): Promise => { + const db = getDB(); + const transaction = db.transaction([GENERATIONS_STORE], 'readonly'); + const store = transaction.objectStore(GENERATIONS_STORE); + const index = store.index('timestamp'); + + return new Promise((resolve, reject) => { + const request = index.getAll(); + request.onsuccess = () => { + // 按时间倒序排列 + const generations = request.result.sort((a, b) => b.timestamp - a.timestamp); + resolve(generations); + }; + request.onerror = () => reject(request.error); + }); +}; + +/** + * 获取所有编辑记录(按时间倒序) + */ +export const getAllEdits = async (): Promise => { + const db = getDB(); + const transaction = db.transaction([EDITS_STORE], 'readonly'); + const store = transaction.objectStore(EDITS_STORE); + const index = store.index('timestamp'); + + return new Promise((resolve, reject) => { + const request = index.getAll(); + request.onsuccess = () => { + // 按时间倒序排列 + const edits = request.result.sort((a, b) => b.timestamp - a.timestamp); + resolve(edits); + }; + request.onerror = () => reject(request.error); + }); +}; + +/** + * 根据父生成ID获取编辑记录 + */ +export const getEditsByParentGenerationId = async (parentGenerationId: string): Promise => { + const db = getDB(); + const transaction = db.transaction([EDITS_STORE], 'readonly'); + const store = transaction.objectStore(EDITS_STORE); + const index = store.index('parentGenerationId'); + + return new Promise((resolve, reject) => { + const request = index.getAll(IDBKeyRange.only(parentGenerationId)); + request.onsuccess = () => { + // 按时间倒序排列 + const edits = request.result.sort((a, b) => b.timestamp - a.timestamp); + resolve(edits); + }; + request.onerror = () => reject(request.error); + }); +}; + +/** + * 删除最旧的记录以保持限制 + */ +export const cleanupOldRecords = async (limit: number = 1000): Promise => { + const db = getDB(); + + // 清理生成记录 + const genTransaction = db.transaction([GENERATIONS_STORE], 'readwrite'); + const genStore = genTransaction.objectStore(GENERATIONS_STORE); + const genIndex = genStore.index('timestamp'); + + // 清理编辑记录 + const editTransaction = db.transaction([EDITS_STORE], 'readwrite'); + const editStore = editTransaction.objectStore(EDITS_STORE); + const editIndex = editStore.index('timestamp'); + + // 获取所有记录并按时间排序 + const allGenerations = await getAllGenerations(); + const allEdits = await getAllEdits(); + + // 计算需要删除的记录数量 + if (allGenerations.length > limit) { + const toDelete = allGenerations.slice(limit); + for (const gen of toDelete) { + genStore.delete(gen.id); + } + } + + if (allEdits.length > limit) { + const toDelete = allEdits.slice(limit); + for (const edit of toDelete) { + editStore.delete(edit.id); + } + } +}; + +/** + * 清空所有记录 + */ +export const clearAllRecords = async (): Promise => { + const db = getDB(); + + const genTransaction = db.transaction([GENERATIONS_STORE], 'readwrite'); + const genStore = genTransaction.objectStore(GENERATIONS_STORE); + + const editTransaction = db.transaction([EDITS_STORE], 'readwrite'); + const editStore = editTransaction.objectStore(EDITS_STORE); + + return Promise.all([ + new Promise((resolve, reject) => { + const request = genStore.clear(); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }), + new Promise((resolve, reject) => { + const request = editStore.clear(); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }) + ]).then(() => undefined); +}; \ No newline at end of file diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 5d745ed..ccb11df 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -2,6 +2,7 @@ import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import { Project, Generation, Edit, SegmentationMask, BrushStroke, UploadResult } from '../types'; import { generateId } from '../utils/imageUtils'; +import * as indexedDBService from '../services/indexedDBService'; // 定义不包含图像数据的轻量级项目结构 interface LightweightProject { @@ -204,29 +205,45 @@ export const useAppStore = create()( return get().blobStore.get(url); }, - addGeneration: (generation) => set((state) => { - // 将base64图像数据转换为Blob并存储 - const sourceAssets = generation.sourceAssets.map(asset => { - if (asset.url.startsWith('data:')) { - // 从base64创建Blob - const base64 = asset.url.split(',')[1]; - const byteString = atob(base64); - const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0]; - const ab = new ArrayBuffer(byteString.length); - const ia = new Uint8Array(ab); - for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); + addGeneration: (generation) => { + // 保存到IndexedDB + indexedDBService.addGeneration(generation).catch(err => { + console.error('保存生成记录到IndexedDB失败:', err); + }); + + set((state) => { + // 将base64图像数据转换为Blob并存储 + const sourceAssets = generation.sourceAssets.map(asset => { + if (asset.url.startsWith('data:')) { + // 从base64创建Blob + const base64 = asset.url.split(',')[1]; + const byteString = atob(base64); + const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + const blob = new Blob([ab], { type: mimeString }); + const blobUrl = URL.createObjectURL(blob); + + // 存储Blob对象 + set((innerState) => { + const newBlobStore = new Map(innerState.blobStore); + newBlobStore.set(blobUrl, blob); + return { blobStore: newBlobStore }; + }); + + return { + id: asset.id, + type: asset.type, + mime: asset.mime, + width: asset.width, + height: asset.height, + checksum: asset.checksum, + blobUrl + }; } - const blob = new Blob([ab], { type: mimeString }); - const blobUrl = URL.createObjectURL(blob); - - // 存储Blob对象 - set((innerState) => { - const newBlobStore = new Map(innerState.blobStore); - newBlobStore.set(blobUrl, blob); - return { blobStore: newBlobStore }; - }); - return { id: asset.id, type: asset.type, @@ -234,164 +251,170 @@ export const useAppStore = create()( width: asset.width, height: asset.height, checksum: asset.checksum, - blobUrl + blobUrl: asset.url }; + }); + + // 将输出资产转换为Blob URL + const outputAssetsBlobUrls = generation.outputAssets.map(asset => { + if (asset.url.startsWith('data:')) { + // 从base64创建Blob + const base64 = asset.url.split(',')[1]; + const byteString = atob(base64); + const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + const blob = new Blob([ab], { type: mimeString }); + const blobUrl = URL.createObjectURL(blob); + + // 存储Blob对象 + set((innerState) => { + const newBlobStore = new Map(innerState.blobStore); + newBlobStore.set(blobUrl, blob); + return { blobStore: newBlobStore }; + }); + + return blobUrl; + } + return asset.url; + }); + + // 创建轻量级生成记录 + const lightweightGeneration = { + id: generation.id, + prompt: generation.prompt, + parameters: generation.parameters, + sourceAssets, + outputAssetsBlobUrls, + modelVersion: generation.modelVersion, + timestamp: generation.timestamp, + uploadResults: generation.uploadResults + }; + + const updatedProject = state.currentProject ? { + ...state.currentProject, + generations: [...state.currentProject.generations, lightweightGeneration], + updatedAt: Date.now() + } : { + // 如果没有项目,创建一个新项目包含此生成记录 + id: generateId(), + title: '未命名项目', + generations: [lightweightGeneration], + edits: [], + createdAt: Date.now(), + updatedAt: Date.now() + }; + + // 清理旧记录以保持在限制内(现在限制为1000条) + if (updatedProject.generations.length > 1000) { + updatedProject.generations.splice(0, updatedProject.generations.length - 1000); + // 同时清理IndexedDB中的旧记录 + indexedDBService.cleanupOldRecords(1000).catch(err => { + console.error('清理IndexedDB旧记录失败:', err); + }); } + return { - id: asset.id, - type: asset.type, - mime: asset.mime, - width: asset.width, - height: asset.height, - checksum: asset.checksum, - blobUrl: asset.url + currentProject: updatedProject }; }); - - // 将输出资产转换为Blob URL - const outputAssetsBlobUrls = generation.outputAssets.map(asset => { - if (asset.url.startsWith('data:')) { - // 从base64创建Blob - const base64 = asset.url.split(',')[1]; - const byteString = atob(base64); - const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0]; - const ab = new ArrayBuffer(byteString.length); - const ia = new Uint8Array(ab); - for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); - } - const blob = new Blob([ab], { type: mimeString }); - const blobUrl = URL.createObjectURL(blob); - - // 存储Blob对象 - set((innerState) => { - const newBlobStore = new Map(innerState.blobStore); - newBlobStore.set(blobUrl, blob); - return { blobStore: newBlobStore }; - }); - - return blobUrl; - } - return asset.url; - }); - - // 创建轻量级生成记录 - const lightweightGeneration = { - id: generation.id, - prompt: generation.prompt, - parameters: generation.parameters, - sourceAssets, - outputAssetsBlobUrls, - modelVersion: generation.modelVersion, - timestamp: generation.timestamp, - uploadResults: generation.uploadResults - }; - - const updatedProject = state.currentProject ? { - ...state.currentProject, - generations: [...state.currentProject.generations, lightweightGeneration], - updatedAt: Date.now() - } : { - // 如果没有项目,创建一个新项目包含此生成记录 - id: generateId(), - title: '未命名项目', - generations: [lightweightGeneration], - edits: [], - createdAt: Date.now(), - updatedAt: Date.now() - }; - - // 清理旧记录以保持在限制内 - if (updatedProject.generations.length > 10) { - updatedProject.generations.splice(0, updatedProject.generations.length - 10); - } - - return { - currentProject: updatedProject - }; - }), + }, - addEdit: (edit) => set((state) => { - // 将遮罩参考资产转换为Blob URL(如果存在) - let maskReferenceAssetBlobUrl: string | undefined; - if (edit.maskReferenceAsset && edit.maskReferenceAsset.url.startsWith('data:')) { - const base64 = edit.maskReferenceAsset.url.split(',')[1]; - const byteString = atob(base64); - const mimeString = edit.maskReferenceAsset.url.split(',')[0].split(':')[1].split(';')[0]; - const ab = new ArrayBuffer(byteString.length); - const ia = new Uint8Array(ab); - for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); - } - const blob = new Blob([ab], { type: mimeString }); - maskReferenceAssetBlobUrl = URL.createObjectURL(blob); - - // 存储Blob对象 - set((innerState) => { - const newBlobStore = new Map(innerState.blobStore); - newBlobStore.set(maskReferenceAssetBlobUrl!, blob); - return { blobStore: newBlobStore }; - }); - } else if (edit.maskReferenceAsset) { - maskReferenceAssetBlobUrl = edit.maskReferenceAsset.url; - } + addEdit: (edit) => { + // 保存到IndexedDB + indexedDBService.addEdit(edit).catch(err => { + console.error('保存编辑记录到IndexedDB失败:', err); + }); - // 将输出资产转换为Blob URL - const outputAssetsBlobUrls = edit.outputAssets.map(asset => { - if (asset.url.startsWith('data:')) { - // 从base64创建Blob - const base64 = asset.url.split(',')[1]; + set((state) => { + // 将遮罩参考资产转换为Blob URL(如果存在) + let maskReferenceAssetBlobUrl: string | undefined; + if (edit.maskReferenceAsset && edit.maskReferenceAsset.url.startsWith('data:')) { + const base64 = edit.maskReferenceAsset.url.split(',')[1]; const byteString = atob(base64); - const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0]; + const mimeString = edit.maskReferenceAsset.url.split(',')[0].split(':')[1].split(';')[0]; const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } const blob = new Blob([ab], { type: mimeString }); - const blobUrl = URL.createObjectURL(blob); + maskReferenceAssetBlobUrl = URL.createObjectURL(blob); // 存储Blob对象 set((innerState) => { const newBlobStore = new Map(innerState.blobStore); - newBlobStore.set(blobUrl, blob); + newBlobStore.set(maskReferenceAssetBlobUrl!, blob); return { blobStore: newBlobStore }; }); - - return blobUrl; + } else if (edit.maskReferenceAsset) { + maskReferenceAssetBlobUrl = edit.maskReferenceAsset.url; } - return asset.url; + + // 将输出资产转换为Blob URL + const outputAssetsBlobUrls = edit.outputAssets.map(asset => { + if (asset.url.startsWith('data:')) { + // 从base64创建Blob + const base64 = asset.url.split(',')[1]; + const byteString = atob(base64); + const mimeString = asset.url.split(',')[0].split(':')[1].split(';')[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + const blob = new Blob([ab], { type: mimeString }); + const blobUrl = URL.createObjectURL(blob); + + // 存储Blob对象 + set((innerState) => { + const newBlobStore = new Map(innerState.blobStore); + newBlobStore.set(blobUrl, blob); + return { blobStore: newBlobStore }; + }); + + return blobUrl; + } + return asset.url; + }); + + // 创建轻量级编辑记录 + const lightweightEdit = { + id: edit.id, + parentGenerationId: edit.parentGenerationId, + maskAssetId: edit.maskAssetId, + maskReferenceAssetBlobUrl, + instruction: edit.instruction, + outputAssetsBlobUrls, + timestamp: edit.timestamp, + uploadResults: edit.uploadResults + }; + + if (!state.currentProject) return {}; + + const updatedProject = { + ...state.currentProject, + edits: [...state.currentProject.edits, lightweightEdit], + updatedAt: Date.now() + }; + + // 清理旧记录以保持在限制内(现在限制为1000条) + if (updatedProject.edits.length > 1000) { + updatedProject.edits.splice(0, updatedProject.edits.length - 1000); + // 同时清理IndexedDB中的旧记录 + indexedDBService.cleanupOldRecords(1000).catch(err => { + console.error('清理IndexedDB旧记录失败:', err); + }); + } + + return { + currentProject: updatedProject + }; }); - - // 创建轻量级编辑记录 - const lightweightEdit = { - id: edit.id, - parentGenerationId: edit.parentGenerationId, - maskAssetId: edit.maskAssetId, - maskReferenceAssetBlobUrl, - instruction: edit.instruction, - outputAssetsBlobUrls, - timestamp: edit.timestamp, - uploadResults: edit.uploadResults - }; - - if (!state.currentProject) return {}; - - const updatedProject = { - ...state.currentProject, - edits: [...state.currentProject.edits, lightweightEdit], - updatedAt: Date.now() - }; - - // 清理旧记录以保持在限制内 - if (updatedProject.edits.length > 10) { - updatedProject.edits.splice(0, updatedProject.edits.length - 10); - } - - return { - currentProject: updatedProject - }; - }), + }, selectGeneration: (id) => set({ selectedGenerationId: id }), selectEdit: (id) => set({ selectedEditId: id }), @@ -401,23 +424,28 @@ export const useAppStore = create()( setSelectedTool: (tool) => set({ selectedTool: tool }), - // 清理旧的历史记录,保留最多10条 + // 清理旧的历史记录,保留最多1000条 cleanupOldHistory: () => set((state) => { if (!state.currentProject) return {}; const generations = [...state.currentProject.generations]; const edits = [...state.currentProject.edits]; - // 如果生成记录超过10条,只保留最新的10条 - if (generations.length > 10) { - generations.splice(0, generations.length - 10); + // 如果生成记录超过1000条,只保留最新的1000条 + if (generations.length > 1000) { + generations.splice(0, generations.length - 1000); } - // 如果编辑记录超过10条,只保留最新的10条 - if (edits.length > 10) { - edits.splice(0, edits.length - 10); + // 如果编辑记录超过1000条,只保留最新的1000条 + if (edits.length > 1000) { + edits.splice(0, edits.length - 1000); } + // 同时清理IndexedDB中的旧记录 + indexedDBService.cleanupOldRecords(1000).catch(err => { + console.error('清理IndexedDB旧记录失败:', err); + }); + return { currentProject: { ...state.currentProject,