You've already forked Nano-Banana-AI-Image-Editor
修复内存泄漏问题,优化Blob URL清理机制
This commit is contained in:
48
package-lock.json
generated
48
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"name": "ano-banana-ai-image-editor",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vite-react-typescript-starter",
|
||||
"name": "ano-banana-ai-image-editor",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.16.0",
|
||||
@@ -21,6 +21,7 @@
|
||||
"konva": "^9.3.22",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^9.10.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-konva": "^18.2.10",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
@@ -359,6 +360,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@date-fns/tz": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
|
||||
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -2915,6 +2922,22 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns-jalali": {
|
||||
"version": "4.1.0-0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
||||
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
@@ -5267,6 +5290,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-day-picker": {
|
||||
"version": "9.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.10.0.tgz",
|
||||
"integrity": "sha512-tedecLSd+fpSN+J08601MaMsf122nxtqZXxB6lwX37qFoLtuPNuRJN8ylxFjLhyJS1kaLfAqL1GUkSLd2BMrpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@date-fns/tz": "^1.4.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-jalali": "^4.1.0-0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/gpbl"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"konva": "^9.3.22",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^9.10.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-konva": "^18.2.10",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,9 +38,18 @@ export const ImageCanvas: React.FC = () => {
|
||||
|
||||
// 加载图像并在 canvasImage 变化时自动适应
|
||||
useEffect(() => {
|
||||
let img: HTMLImageElement | null = null;
|
||||
|
||||
if (canvasImage) {
|
||||
const img = new window.Image();
|
||||
img = new window.Image();
|
||||
let isCancelled = false;
|
||||
|
||||
img.onload = () => {
|
||||
// 检查是否已取消
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setImage(img);
|
||||
|
||||
// 每次有新图像时都自动适应画布,而不仅仅是在初始状态下
|
||||
@@ -59,10 +68,26 @@ export const ImageCanvas: React.FC = () => {
|
||||
// 居中图像
|
||||
setCanvasPan({ x: 0, y: 0 });
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
if (!isCancelled) {
|
||||
console.error('图像加载失败');
|
||||
}
|
||||
};
|
||||
|
||||
img.src = canvasImage;
|
||||
} else {
|
||||
setImage(null);
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
// 取消图像加载
|
||||
if (img) {
|
||||
img.onload = null;
|
||||
img.onerror = null;
|
||||
}
|
||||
};
|
||||
}, [canvasImage, stageSize, setCanvasZoom, setCanvasPan]);
|
||||
|
||||
// 处理舞台大小调整
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as indexedDBService from '../services/indexedDBService';
|
||||
|
||||
export const useIndexedDBListener = () => {
|
||||
@@ -6,44 +6,73 @@ 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 () => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const allGenerations = await indexedDBService.getAllGenerations();
|
||||
const allEdits = await indexedDBService.getAllEdits();
|
||||
setGenerations(allGenerations);
|
||||
setEdits(allEdits);
|
||||
setError(null);
|
||||
if (isMountedRef.current) {
|
||||
setGenerations(allGenerations);
|
||||
setEdits(allEdits);
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('从IndexedDB加载记录失败:', err);
|
||||
setError('加载历史记录失败');
|
||||
if (isMountedRef.current) {
|
||||
setError('加载历史记录失败');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 标记组件已挂载
|
||||
isMountedRef.current = true;
|
||||
|
||||
// 初始化数据库并加载记录
|
||||
const initAndLoad = async () => {
|
||||
try {
|
||||
await indexedDBService.initDB();
|
||||
await loadRecords();
|
||||
if (isMountedRef.current) {
|
||||
await loadRecords();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('初始化IndexedDB失败:', err);
|
||||
setError('初始化数据库失败');
|
||||
setLoading(false);
|
||||
if (isMountedRef.current) {
|
||||
setError('初始化数据库失败');
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initAndLoad();
|
||||
|
||||
// 设置定时器定期检查新记录
|
||||
const interval = setInterval(() => {
|
||||
loadRecords();
|
||||
intervalRef.current = setInterval(() => {
|
||||
if (isMountedRef.current) {
|
||||
loadRecords();
|
||||
}
|
||||
}, 3000); // 每3秒检查一次
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// 清理函数
|
||||
return () => {
|
||||
// 标记组件已卸载
|
||||
isMountedRef.current = false;
|
||||
|
||||
// 清除定时器
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refresh = () => {
|
||||
|
||||
@@ -62,6 +62,24 @@ export class GeminiService {
|
||||
if (candidate.finishReason === 'IMAGE_SAFETY') {
|
||||
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
|
||||
}
|
||||
// 检查finishReason为STOP但没有inlineData的情况
|
||||
if (candidate.finishReason === 'STOP') {
|
||||
// 检查是否有inlineData
|
||||
let hasInlineData = false;
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData) {
|
||||
hasInlineData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有inlineData,则抛出错误
|
||||
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
|
||||
throw new Error(candidate.content.parts[0].text || '生成失败:未返回图像数据');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const images: string[] = []
|
||||
@@ -132,6 +150,24 @@ export class GeminiService {
|
||||
if (candidate.finishReason === 'PROHIBITED_CONTENT') {
|
||||
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
|
||||
}
|
||||
// 检查finishReason为STOP但没有inlineData的情况
|
||||
if (candidate.finishReason === 'STOP') {
|
||||
// 检查是否有inlineData
|
||||
let hasInlineData = false;
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData) {
|
||||
hasInlineData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有inlineData,则抛出错误
|
||||
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
|
||||
throw new Error(candidate.content.parts[0].text || '编辑失败:未返回图像数据');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const images: string[] = []
|
||||
@@ -196,6 +232,24 @@ export class GeminiService {
|
||||
if (candidate.finishReason === 'PROHIBITED_CONTENT') {
|
||||
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
|
||||
}
|
||||
// 检查finishReason为STOP但没有inlineData的情况
|
||||
if (candidate.finishReason === 'STOP') {
|
||||
// 检查是否有inlineData
|
||||
let hasInlineData = false;
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData) {
|
||||
hasInlineData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有inlineData,则抛出错误
|
||||
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
|
||||
throw new Error(candidate.content.parts[0].text || '分割失败:未返回结果数据');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const responseText = response.candidates[0].content.parts[0].text
|
||||
|
||||
@@ -7,6 +7,49 @@ const UPLOAD_URL = 'https://api.pandorastudio.cn/auth/OSSupload'
|
||||
// 创建一个Map来缓存已上传的图像
|
||||
const uploadCache = new Map<string, UploadResult>()
|
||||
|
||||
// 缓存配置
|
||||
const MAX_CACHE_SIZE = 100; // 最大缓存条目数
|
||||
const CACHE_EXPIRY_TIME = 30 * 60 * 1000; // 缓存过期时间30分钟
|
||||
|
||||
/**
|
||||
* 清理过期的缓存条目
|
||||
*/
|
||||
function cleanupExpiredCache(): void {
|
||||
const now = Date.now();
|
||||
let deletedCount = 0;
|
||||
|
||||
uploadCache.forEach((value, key) => {
|
||||
if (now - value.timestamp > CACHE_EXPIRY_TIME) {
|
||||
uploadCache.delete(key);
|
||||
deletedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (deletedCount > 0) {
|
||||
console.log(`清除了 ${deletedCount} 个过期的缓存条目`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并维护缓存大小
|
||||
*/
|
||||
function maintainCacheSize(): void {
|
||||
// 如果缓存大小超过限制,删除最旧的条目
|
||||
if (uploadCache.size >= MAX_CACHE_SIZE) {
|
||||
// 获取所有条目并按时间排序
|
||||
const entries = Array.from(uploadCache.entries());
|
||||
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||
|
||||
// 删除最旧的条目,直到缓存大小在限制内
|
||||
const deleteCount = Math.max(1, Math.floor(MAX_CACHE_SIZE * 0.1)); // 删除10%的条目
|
||||
for (let i = 0; i < deleteCount && uploadCache.size >= MAX_CACHE_SIZE; i++) {
|
||||
uploadCache.delete(entries[i][0]);
|
||||
}
|
||||
|
||||
console.log(`缓存已满,删除了 ${deleteCount} 个最旧的条目`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图像的唯一标识符
|
||||
* @param base64Data - base64编码的图像数据
|
||||
@@ -35,8 +78,15 @@ export const uploadImage = async (base64Data: string, accessToken: string, skipC
|
||||
const imageHash = getImageHash(base64Data)
|
||||
|
||||
if (!skipCache && uploadCache.has(imageHash)) {
|
||||
console.log('从缓存中获取上传结果')
|
||||
return uploadCache.get(imageHash)!
|
||||
const cachedResult = uploadCache.get(imageHash)!;
|
||||
// 检查缓存是否过期
|
||||
if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) {
|
||||
console.log('从缓存中获取上传结果')
|
||||
return cachedResult;
|
||||
} else {
|
||||
// 缓存过期,删除它
|
||||
uploadCache.delete(imageHash);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -73,6 +123,12 @@ export const uploadImage = async (base64Data: string, accessToken: string, skipC
|
||||
const uploadAssetUrl = import.meta.env.VITE_UPLOAD_ASSET_URL || ''
|
||||
const fullUrl = uploadAssetUrl ? `${uploadAssetUrl}/${result.data}` : result.data
|
||||
|
||||
// 清理过期缓存
|
||||
cleanupExpiredCache();
|
||||
|
||||
// 维护缓存大小
|
||||
maintainCacheSize();
|
||||
|
||||
// 将上传结果存储到缓存中
|
||||
const uploadResult = { success: true, url: fullUrl, error: undefined }
|
||||
uploadCache.set(imageHash, {
|
||||
@@ -88,6 +144,12 @@ export const uploadImage = async (base64Data: string, accessToken: string, skipC
|
||||
console.error('上传图像时出错:', error)
|
||||
const errorResult = { success: false, url: undefined, error: error instanceof Error ? error.message : String(error) }
|
||||
|
||||
// 清理过期缓存
|
||||
cleanupExpiredCache();
|
||||
|
||||
// 维护缓存大小(即使是失败的结果也缓存,但时间较短)
|
||||
maintainCacheSize();
|
||||
|
||||
// 将失败的上传结果也存储到缓存中(可选)
|
||||
uploadCache.set(imageHash, {
|
||||
...errorResult,
|
||||
@@ -152,4 +214,5 @@ export const uploadImages = async (base64Images: string[], accessToken: string,
|
||||
*/
|
||||
export const clearUploadCache = (): void => {
|
||||
uploadCache.clear()
|
||||
console.log('上传缓存已清除')
|
||||
}
|
||||
|
||||
@@ -123,6 +123,10 @@ interface AppState {
|
||||
addBlob: (blob: Blob) => string;
|
||||
getBlob: (url: string) => Blob | undefined;
|
||||
cleanupOldHistory: () => void;
|
||||
|
||||
// Blob URL清理操作
|
||||
revokeBlobUrls: (urls: string[]) => void;
|
||||
cleanupAllBlobUrls: () => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()(
|
||||
@@ -314,6 +318,36 @@ export const useAppStore = create<AppState>()(
|
||||
|
||||
// 清理旧记录以保持在限制内(现在限制为1000条)
|
||||
if (updatedProject.generations.length > 1000) {
|
||||
// 收集需要释放的Blob URLs
|
||||
const urlsToRevoke: string[] = [];
|
||||
const generationsToRemove = updatedProject.generations.slice(0, updatedProject.generations.length - 1000);
|
||||
generationsToRemove.forEach(gen => {
|
||||
gen.sourceAssets.forEach(asset => {
|
||||
if (asset.blobUrl.startsWith('blob:')) {
|
||||
urlsToRevoke.push(asset.blobUrl);
|
||||
}
|
||||
});
|
||||
gen.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;
|
||||
});
|
||||
}
|
||||
|
||||
// 清理数组
|
||||
updatedProject.generations.splice(0, updatedProject.generations.length - 1000);
|
||||
// 同时清理IndexedDB中的旧记录
|
||||
indexedDBService.cleanupOldRecords(1000).catch(err => {
|
||||
@@ -409,6 +443,34 @@ export const useAppStore = create<AppState>()(
|
||||
|
||||
// 清理旧记录以保持在限制内(现在限制为1000条)
|
||||
if (updatedProject.edits.length > 1000) {
|
||||
// 收集需要释放的Blob URLs
|
||||
const urlsToRevoke: string[] = [];
|
||||
const editsToRemove = updatedProject.edits.slice(0, updatedProject.edits.length - 1000);
|
||||
editsToRemove.forEach(edit => {
|
||||
if (edit.maskReferenceAssetBlobUrl && edit.maskReferenceAssetBlobUrl.startsWith('blob:')) {
|
||||
urlsToRevoke.push(edit.maskReferenceAssetBlobUrl);
|
||||
}
|
||||
edit.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;
|
||||
});
|
||||
}
|
||||
|
||||
// 清理数组
|
||||
updatedProject.edits.splice(0, updatedProject.edits.length - 1000);
|
||||
// 同时清理IndexedDB中的旧记录
|
||||
indexedDBService.cleanupOldRecords(1000).catch(err => {
|
||||
@@ -437,16 +499,56 @@ export const useAppStore = create<AppState>()(
|
||||
const generations = [...state.currentProject.generations];
|
||||
const edits = [...state.currentProject.edits];
|
||||
|
||||
// 收集需要释放的Blob URLs
|
||||
const urlsToRevoke: string[] = [];
|
||||
|
||||
// 如果生成记录超过1000条,只保留最新的1000条
|
||||
if (generations.length > 1000) {
|
||||
const generationsToRemove = generations.slice(0, generations.length - 1000);
|
||||
generationsToRemove.forEach(gen => {
|
||||
gen.sourceAssets.forEach(asset => {
|
||||
if (asset.blobUrl.startsWith('blob:')) {
|
||||
urlsToRevoke.push(asset.blobUrl);
|
||||
}
|
||||
});
|
||||
gen.outputAssetsBlobUrls.forEach(url => {
|
||||
if (url.startsWith('blob:')) {
|
||||
urlsToRevoke.push(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
generations.splice(0, generations.length - 1000);
|
||||
}
|
||||
|
||||
// 如果编辑记录超过1000条,只保留最新的1000条
|
||||
if (edits.length > 1000) {
|
||||
const editsToRemove = edits.slice(0, edits.length - 1000);
|
||||
editsToRemove.forEach(edit => {
|
||||
if (edit.maskReferenceAssetBlobUrl && edit.maskReferenceAssetBlobUrl.startsWith('blob:')) {
|
||||
urlsToRevoke.push(edit.maskReferenceAssetBlobUrl);
|
||||
}
|
||||
edit.outputAssetsBlobUrls.forEach(url => {
|
||||
if (url.startsWith('blob:')) {
|
||||
urlsToRevoke.push(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
edits.splice(0, edits.length - 1000);
|
||||
}
|
||||
|
||||
// 释放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.cleanupOldRecords(1000).catch(err => {
|
||||
console.error('清理IndexedDB旧记录失败:', err);
|
||||
@@ -460,6 +562,27 @@ export const useAppStore = create<AppState>()(
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
};
|
||||
}),
|
||||
|
||||
// 释放指定的Blob URLs
|
||||
revokeBlobUrls: (urls: string[]) => set((state) => {
|
||||
urls.forEach(url => {
|
||||
if (state.blobStore.has(url)) {
|
||||
URL.revokeObjectURL(url);
|
||||
const newBlobStore = new Map(state.blobStore);
|
||||
newBlobStore.delete(url);
|
||||
state = { ...state, blobStore: newBlobStore };
|
||||
}
|
||||
});
|
||||
return state;
|
||||
}),
|
||||
|
||||
// 释放所有Blob URLs
|
||||
cleanupAllBlobUrls: () => set((state) => {
|
||||
state.blobStore.forEach((_, url) => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
return { ...state, blobStore: new Map() };
|
||||
})
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -7,4 +7,11 @@ export default defineConfig({
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'react-day-picker/dist/locale/zh-CN': 'date-fns/locale/zh-CN',
|
||||
'react-day-picker/dist/locale': 'date-fns/locale',
|
||||
'react-day-picker/locale': 'date-fns/locale',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user