6 Commits
futrue ... main

Author SHA1 Message Date
3ba2a0cbd5 Merge branch 'futrue' 2025-09-22 22:42:40 +08:00
d4f9735f88 分支合并 2025-09-19 23:46:38 +08:00
yuantao
4b5b1a5eba 新增 历史记录删除功能 2025-09-19 18:40:43 +08:00
yuantao
eae15ced5a 新增常用提示词功能 2025-09-19 17:25:46 +08:00
yuantao
70684b2ddf 优化 调整了历史记录预览窗口的实现 2025-09-19 17:00:51 +08:00
yuantao
9a5e4d8041 添加了IFLOW描述文件;
优化 调整了历史记录悬浮窗的显示位置;
2025-09-19 16:31:09 +08:00
5 changed files with 260 additions and 136 deletions

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useAppStore } from '../store/useAppStore'; import { useAppStore } from '../store/useAppStore';
import { Button } from './ui/Button'; import { Button } from './ui/Button';
import { History, Download, Image as ImageIcon, Trash2 } from 'lucide-react'; import { History, Download, Trash2, 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 { useIndexedDBListener } from '../hooks/useIndexedDBListener'; import { useIndexedDBListener } from '../hooks/useIndexedDBListener';
@@ -271,18 +271,47 @@ export const HistoryPanel: React.FC<{
); );
} }
// 监听鼠标离开窗口事件,确保悬浮预览正确关闭
useEffect(() => {
const handleMouseLeave = (e: MouseEvent) => {
// 当鼠标离开浏览器窗口时,关闭悬浮预览
if (e.relatedTarget === null) {
setHoveredImage(null);
if (setPreviewPosition) {
setPreviewPosition(null);
}
}
};
const handleBlur = () => {
// 当窗口失去焦点时,关闭悬浮预览
setHoveredImage(null);
if (setPreviewPosition) {
setPreviewPosition(null);
}
};
window.addEventListener('mouseleave', handleMouseLeave);
window.addEventListener('blur', handleBlur);
return () => {
window.removeEventListener('mouseleave', handleMouseLeave);
window.removeEventListener('blur', handleBlur);
};
}, [setHoveredImage, setPreviewPosition]);
if (!showHistory) { if (!showHistory) {
return ( return (
<div className="w-8 bg-white flex flex-col items-center justify-center rounded-r-xl"> <div className="w-8 bg-white flex flex-col items-center justify-center rounded-r-xl overflow-hidden">
<button <button
onClick={() => setShowHistory(true)} onClick={() => setShowHistory(true)}
className="w-6 h-16 bg-gray-100 hover:bg-gray-200 rounded-l-lg flex items-center justify-center transition-colors group" className="w-6 h-16 bg-gray-100 hover:bg-gray-200 rounded-l-lg flex items-center justify-center transition-all duration-300 ease-in-out group"
title="显示历史面板" title="显示历史面板"
> >
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div> <div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full transition-colors duration-200"></div>
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div> <div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full transition-colors duration-200"></div>
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div> <div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full transition-colors duration-200"></div>
</div> </div>
</button> </button>
</div> </div>

View File

@@ -376,16 +376,16 @@ export const PromptComposer: React.FC = () => {
if (!showPromptPanel) { if (!showPromptPanel) {
return ( return (
<div className="w-8 bg-white flex flex-col items-center justify-center rounded-l-xl"> <div className="w-8 bg-white flex flex-col items-center justify-center rounded-l-xl overflow-hidden">
<button <button
onClick={() => setShowPromptPanel(true)} onClick={() => setShowPromptPanel(true)}
className="w-6 h-16 bg-gray-100 hover:bg-gray-200 rounded-r-lg flex items-center justify-center transition-all duration-300 ease-in-out group" className="w-6 h-16 bg-gray-100 hover:bg-gray-200 rounded-r-lg flex items-center justify-center transition-all duration-300 ease-in-out group"
title="显示提示面板" title="显示提示面板"
> >
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div> <div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full transition-colors duration-200"></div>
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div> <div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full transition-colors duration-200"></div>
<div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full"></div> <div className="w-1 h-1 bg-gray-500 group-hover:bg-gray-400 rounded-full transition-colors duration-200"></div>
</div> </div>
</button> </button>
</div> </div>

View File

@@ -194,7 +194,7 @@ export const useImageGeneration = () => {
id: generateId(), id: generateId(),
type: 'original' as const, type: 'original' as const,
url: blobUrl, // 存储Blob URL而不是base64 url: blobUrl, // 存储Blob URL而不是base64
mime: 'image/png', mime: blob.type || 'image/png',
width: 1024, width: 1024,
height: 1024, height: 1024,
checksum checksum

View File

@@ -2,31 +2,31 @@
import { UploadResult } from '../types' import { UploadResult } from '../types'
// 上传接口URL // 上传接口URL
const UPLOAD_URL = 'https://api.pandorastudio.cn/auth/OSSupload' const UPLOAD_URL = import.meta.env.VITE_UPLOAD_API
// 创建一个Map来缓存已上传的图像 // 创建一个Map来缓存已上传的图像
const uploadCache = new Map<string, UploadResult>() const uploadCache = new Map<string, UploadResult>()
// 缓存配置 // 缓存配置
const MAX_CACHE_SIZE = 50; // 减少最大缓存条目数 const MAX_CACHE_SIZE = 20 // 减少最大缓存条目数
const CACHE_EXPIRY_TIME = 15 * 60 * 1000; // 缓存过期时间15分钟 const CACHE_EXPIRY_TIME = 15 * 60 * 1000 // 缓存过期时间15分钟
/** /**
* 清理过期的缓存条目 * 清理过期的缓存条目
*/ */
function cleanupExpiredCache(): void { function cleanupExpiredCache(): void {
const now = Date.now(); const now = Date.now()
let deletedCount = 0; let deletedCount = 0
uploadCache.forEach((value, key) => { uploadCache.forEach((value, key) => {
if (now - value.timestamp > CACHE_EXPIRY_TIME) { if (now - value.timestamp > CACHE_EXPIRY_TIME) {
uploadCache.delete(key); uploadCache.delete(key)
deletedCount++; deletedCount++
} }
}); })
if (deletedCount > 0) { if (deletedCount > 0) {
console.log(`清除了 ${deletedCount} 个过期的缓存条目`); console.log(`清除了 ${deletedCount} 个过期的缓存条目`)
} }
} }
@@ -37,16 +37,16 @@ function maintainCacheSize(): void {
// 如果缓存大小超过限制,删除最旧的条目 // 如果缓存大小超过限制,删除最旧的条目
if (uploadCache.size >= MAX_CACHE_SIZE) { if (uploadCache.size >= MAX_CACHE_SIZE) {
// 获取所有条目并按时间排序 // 获取所有条目并按时间排序
const entries = Array.from(uploadCache.entries()); const entries = Array.from(uploadCache.entries())
entries.sort((a, b) => a[1].timestamp - b[1].timestamp); entries.sort((a, b) => a[1].timestamp - b[1].timestamp)
// 删除最旧的条目,直到缓存大小在限制内 // 删除最旧的条目,直到缓存大小在限制内
const deleteCount = Math.max(1, Math.floor(MAX_CACHE_SIZE * 0.2)); // 删除20%的条目 const deleteCount = Math.max(1, Math.floor(MAX_CACHE_SIZE * 0.2)) // 删除20%的条目
for (let i = 0; i < deleteCount && uploadCache.size >= MAX_CACHE_SIZE; i++) { for (let i = 0; i < deleteCount && uploadCache.size >= MAX_CACHE_SIZE; i++) {
uploadCache.delete(entries[i][0]); uploadCache.delete(entries[i][0])
} }
console.log(`缓存已满,删除了 ${deleteCount} 个最旧的条目`); console.log(`缓存已满,删除了 ${deleteCount} 个最旧的条目`)
} }
} }
@@ -60,17 +60,22 @@ function getImageHash(imageData: string): string {
if (imageData.startsWith('blob:')) { if (imageData.startsWith('blob:')) {
// 对于Blob URL我们使用URL本身作为标识符的一部分 // 对于Blob URL我们使用URL本身作为标识符的一部分
// 这不是完美的解决方案,但对于大多数情况足够了 // 这不是完美的解决方案,但对于大多数情况足够了
return btoa(imageData).slice(0, 32); try {
return btoa(imageData).slice(0, 32)
} catch (e) {
// 如果btoa失败例如包含非Latin1字符使用encodeURIComponent
return btoa(encodeURIComponent(imageData)).slice(0, 32)
}
} }
// 对于base64数据使用简单的哈希函数生成图像标识符 // 对于base64数据使用简单的哈希函数生成图像标识符
let hash = 0; let hash = 0
for (let i = 0; i < imageData.length; i++) { for (let i = 0; i < imageData.length; i++) {
const char = imageData.charCodeAt(i); const char = imageData.charCodeAt(i)
hash = ((hash << 5) - hash) + char; hash = (hash << 5) - hash + char
hash = hash & hash; // 转换为32位整数 hash = hash & hash // 转换为32位整数
} }
return hash.toString(); return hash.toString()
} }
/** /**
@@ -81,40 +86,19 @@ function getImageHash(imageData: string): string {
async function getBlobFromUrl(blobUrl: string): Promise<Blob> { async function getBlobFromUrl(blobUrl: string): Promise<Blob> {
try { try {
// 从AppStore获取Blob // 从AppStore获取Blob
const { useAppStore } = await import('../store/useAppStore'); const { useAppStore } = await import('../store/useAppStore')
const blob = useAppStore.getState().getBlob(blobUrl); const blob = useAppStore.getState().getBlob(blobUrl)
if (!blob) { if (!blob) {
// 如果AppStore中没有找到Blob尝试从URL获取 throw new Error('无法从AppStore获取BlobBlob可能已被清理');
console.warn('无法从AppStore获取Blob尝试从URL获取:', blobUrl);
try {
const response = await fetch(blobUrl);
if (!response.ok) {
throw new Error(`获取Blob失败: ${response.status} ${response.statusText}`);
}
return await response.blob();
} catch (error) {
console.error('从URL获取Blob失败:', error);
throw new Error('无法从Blob URL获取图像数据');
}
} }
return blob; return blob;
} catch (error) { } catch (error) {
console.error('从AppStore获取Blob时出错:', error); console.error('从AppStore获取Blob时出错:', error);
// 如果导入AppStore失败直接尝试从URL获取
try {
const response = await fetch(blobUrl);
if (!response.ok) {
throw new Error(`获取Blob失败: ${response.status} ${response.statusText}`);
}
return await response.blob();
} catch (fetchError) {
console.error('从URL获取Blob失败:', fetchError);
throw new Error('无法从Blob URL获取图像数据'); throw new Error('无法从Blob URL获取图像数据');
} }
} }
}
/** /**
* 将图像数据上传到指定接口 * 将图像数据上传到指定接口
@@ -125,46 +109,53 @@ async function getBlobFromUrl(blobUrl: string): Promise<Blob> {
*/ */
export const uploadImage = async (imageData: string | Blob, accessToken: string, skipCache: boolean = false): Promise<{ success: boolean; url?: string; error?: string }> => { export const uploadImage = async (imageData: string | Blob, accessToken: string, skipCache: boolean = false): Promise<{ success: boolean; url?: string; error?: string }> => {
// 检查缓存中是否已有该图像的上传结果 // 检查缓存中是否已有该图像的上传结果
const imageHash = typeof imageData === 'string' ? getImageHash(imageData) : 'blob-' + Date.now(); const imageHash = typeof imageData === 'string' ? getImageHash(imageData) : 'blob-' + Date.now()
if (!skipCache && typeof imageData === 'string' && uploadCache.has(imageHash)) { if (!skipCache && typeof imageData === 'string' && uploadCache.has(imageHash)) {
const cachedResult = uploadCache.get(imageHash)!; const cachedResult = uploadCache.get(imageHash)!
// 检查缓存是否过期 // 检查缓存是否过期
if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) { if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) {
console.log('从缓存中获取上传结果'); console.log('从缓存中获取上传结果')
return cachedResult; // 确保返回的数据结构与新上传的结果一致
return {
success: cachedResult.success,
url: cachedResult.url,
error: cachedResult.error
}
} else { } else {
// 缓存过期,删除它 // 缓存过期,删除它
uploadCache.delete(imageHash); uploadCache.delete(imageHash)
} }
} }
try { try {
let blob: Blob; let blob: Blob
if (typeof imageData === 'string') { if (typeof imageData === 'string') {
if (imageData.startsWith('blob:')) { if (imageData.startsWith('blob:')) {
// 从Blob URL获取Blob数据 // 从Blob URL获取Blob数据
blob = await getBlobFromUrl(imageData); blob = await getBlobFromUrl(imageData)
} else if (imageData.includes('base64,')) { } else if (imageData.includes('base64,')) {
// 从base64数据创建Blob // 从base64数据创建Blob
const base64Data = imageData.split('base64,')[1]; const base64Data = imageData.split('base64,')[1]
const byteString = atob(base64Data); const byteString = atob(base64Data)
const mimeString = 'image/png'; // 默认MIME类型 // 从base64数据中提取MIME类型
const ab = new ArrayBuffer(byteString.length); const mimeMatch = imageData.match(/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/)
const ia = new Uint8Array(ab); const mimeString = mimeMatch ? mimeMatch[1] : 'image/png' // 默认MIME类型
const ab = new ArrayBuffer(byteString.length)
const ia = new Uint8Array(ab)
for (let i = 0; i < byteString.length; i++) { for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i); ia[i] = byteString.charCodeAt(i)
} }
blob = new Blob([ab], { type: mimeString }); blob = new Blob([ab], { type: mimeString })
} else { } else {
// 从URL获取Blob // 从URL获取Blob
const response = await fetch(imageData); const response = await fetch(imageData)
blob = await response.blob(); blob = await response.blob()
} }
} else { } else {
// 如果已经是Blob对象直接使用 // 如果已经是Blob对象直接使用
blob = imageData; blob = imageData
} }
// 创建FormData对象使用唯一文件名 // 创建FormData对象使用唯一文件名
@@ -178,68 +169,69 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
const response = await fetch(UPLOAD_URL, { const response = await fetch(UPLOAD_URL, {
method: 'POST', method: 'POST',
headers: { headers: {
'accessToken': accessToken, accessToken: accessToken,
// 添加其他可能需要的头部 // 添加其他可能需要的头部
}, },
body: formData, body: formData,
}); })
// 记录响应状态以帮助调试 // 记录响应状态以帮助调试
console.log('上传响应状态:', response.status, response.statusText); console.log('上传响应状态:', response.status, response.statusText)
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text()
console.error('上传失败响应内容:', errorText); console.error('上传失败响应内容:', errorText)
throw new Error(`上传失败: ${response.status} ${response.statusText} - ${errorText}`); throw new Error(`上传失败: ${response.status} ${response.statusText} - ${errorText}`)
} }
const result = await response.json(); const result = await response.json()
console.log('上传响应结果:', result); console.log('上传响应结果:', result)
// 根据返回格式处理结果: {"code": 200,"msg": "上传成功","data": "9ecbaa0a0.jpg"} // 根据返回格式处理结果: {"code": 200,"msg": "上传成功","data": "9ecbaa0a0.jpg"}
if (result.code === 200) { if (result.code === 200) {
// 使用环境变量中的VITE_UPLOAD_ASSET_URL作为前缀 // 使用环境变量中的VITE_UPLOAD_ASSET_URL作为前缀
const uploadAssetUrl = import.meta.env.VITE_UPLOAD_ASSET_URL || ''; const uploadAssetUrl = import.meta.env.VITE_UPLOAD_ASSET_URL || ''
const fullUrl = uploadAssetUrl ? `${uploadAssetUrl}/${result.data}` : result.data; const fullUrl = uploadAssetUrl ? `${uploadAssetUrl}/${result.data}` : result.data
// 清理过期缓存 // 清理过期缓存
cleanupExpiredCache(); cleanupExpiredCache()
// 维护缓存大小 // 维护缓存大小
maintainCacheSize(); maintainCacheSize()
// 将上传结果存储到缓存中 // 将上传结果存储到缓存中
const uploadResult = { success: true, url: fullUrl, error: undefined }; const uploadResult = { success: true, url: fullUrl, error: undefined }
if (typeof imageData === 'string') { if (typeof imageData === 'string') {
uploadCache.set(imageHash, { uploadCache.set(imageHash, {
...uploadResult, ...uploadResult,
timestamp: Date.now() timestamp: Date.now(),
}); })
} }
return uploadResult; return { success: true, url: fullUrl, error: undefined }
} else { } else {
throw new Error(`上传失败: ${result.msg}`); throw new Error(`上传失败: ${result.msg}`)
} }
} catch (error) { } catch (error) {
console.error('上传图像时出错:', error); console.error('上传图像时出错:', error)
const errorResult = { success: false, error: error instanceof Error ? error.message : String(error) }; const errorMessage = error instanceof Error ? error.message : String(error)
const errorResult = { success: false, error: errorMessage }
// 清理过期缓存 // 清理过期缓存
cleanupExpiredCache(); cleanupExpiredCache()
// 维护缓存大小(即使是失败的结果也缓存,但时间较短) // 维护缓存大小(即使是失败的结果也缓存,但时间较短)
maintainCacheSize(); maintainCacheSize()
// 将失败的上传结果也存储到缓存中(可选) // 将失败的上传结果也存储到缓存中(可选)
if (typeof imageData === 'string') { if (typeof imageData === 'string') {
uploadCache.set(imageHash, { uploadCache.set(imageHash, {
...errorResult, ...errorResult,
timestamp: Date.now() timestamp: Date.now(),
}); })
} }
return errorResult; return { success: false, error: errorMessage }
} }
} }
@@ -252,43 +244,43 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
*/ */
export const uploadImages = async (imageDatas: (string | Blob)[], accessToken: string, skipCache: boolean = false): Promise<UploadResult[]> => { export const uploadImages = async (imageDatas: (string | Blob)[], accessToken: string, skipCache: boolean = false): Promise<UploadResult[]> => {
try { try {
const results: UploadResult[] = []; const results: UploadResult[] = []
for (let i = 0; i < imageDatas.length; i++) { for (let i = 0; i < imageDatas.length; i++) {
const imageData = imageDatas[i]; const imageData = imageDatas[i]
try { try {
const uploadResult = await uploadImage(imageData, accessToken, skipCache); const uploadResult = await uploadImage(imageData, accessToken, skipCache)
const result: UploadResult = { const result: UploadResult = {
success: uploadResult.success, success: uploadResult.success,
url: uploadResult.url, url: uploadResult.url,
error: uploadResult.error, error: uploadResult.error,
timestamp: Date.now(), timestamp: Date.now(),
}; }
results.push(result); results.push(result)
console.log(`${i + 1}张图像上传${uploadResult.success ? '成功' : '失败'}:`, uploadResult); console.log(`${i + 1}张图像上传${uploadResult.success ? '成功' : '失败'}:`, uploadResult)
} catch (error) { } catch (error) {
const result: UploadResult = { const result: UploadResult = {
success: false, success: false,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
timestamp: Date.now(), timestamp: Date.now(),
}; }
results.push(result); results.push(result)
console.error(`${i + 1}张图像上传失败:`, error); console.error(`${i + 1}张图像上传失败:`, error)
} }
} }
// 检查是否有任何上传失败 // 检查是否有任何上传失败
const failedUploads = results.filter(r => !r.success); const failedUploads = results.filter(r => !r.success)
if (failedUploads.length > 0) { if (failedUploads.length > 0) {
console.warn(`${failedUploads.length}张图像上传失败`); console.warn(`${failedUploads.length}张图像上传失败`)
} else { } else {
console.log(`所有${results.length}张图像上传成功`); console.log(`所有${results.length}张图像上传成功`)
} }
return results; return results
} catch (error) { } catch (error) {
console.error('批量上传图像时出错:', error); console.error('批量上传图像时出错:', error)
throw error; throw error
} }
} }
@@ -296,6 +288,6 @@ export const uploadImages = async (imageDatas: (string | Blob)[], accessToken: s
* 清除上传缓存 * 清除上传缓存
*/ */
export const clearUploadCache = (): void => { export const clearUploadCache = (): void => {
uploadCache.clear(); uploadCache.clear()
console.log('上传缓存已清除'); console.log('上传缓存已清除')
} }

View File

@@ -315,6 +315,17 @@ export const useAppStore = create<AppState>()(
}; };
} else if (asset.url.startsWith('blob:')) { } else if (asset.url.startsWith('blob:')) {
// 如果已经是Blob URL直接使用 // 如果已经是Blob URL直接使用
// 同时确保存储在blobStore中
set((innerState) => {
const blob = innerState.blobStore.get(asset.url);
if (blob) {
const newBlobStore = new Map(innerState.blobStore);
newBlobStore.set(asset.url, blob);
return { blobStore: newBlobStore };
}
return innerState;
});
return { return {
id: asset.id, id: asset.id,
type: asset.type, type: asset.type,
@@ -325,7 +336,7 @@ export const useAppStore = create<AppState>()(
blobUrl: asset.url blobUrl: asset.url
}; };
} }
// 对于其他URL类型创建一个新的Blob URL // 对于其他URL类型直接使用URL
return { return {
id: asset.id, id: asset.id,
type: asset.type, type: asset.type,
@@ -527,6 +538,98 @@ export const useAppStore = create<AppState>()(
setSelectedTool: (tool) => set({ selectedTool: tool }), setSelectedTool: (tool) => set({ selectedTool: tool }),
// 删除生成记录
removeGeneration: (id) => set((state) => {
if (!state.currentProject) return {};
// 收集需要释放的Blob URLs
const urlsToRevoke: string[] = [];
const generationToRemove = state.currentProject.generations.find(gen => gen.id === id);
if (generationToRemove) {
// 收集要删除的生成记录中的Blob URLs
generationToRemove.sourceAssets.forEach(asset => {
if (asset.blobUrl.startsWith('blob:')) {
urlsToRevoke.push(asset.blobUrl);
}
});
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;
});
}
}
// 从项目中移除生成记录
const updatedProject = {
...state.currentProject,
generations: state.currentProject.generations.filter(gen => gen.id !== id),
updatedAt: Date.now()
};
return {
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
};
}),
// 清理旧的历史记录 // 清理旧的历史记录
cleanupOldHistory: () => set((state) => { cleanupOldHistory: () => set((state) => {
if (!state.currentProject) return {}; if (!state.currentProject) return {};