You've already forked Nano-Banana-AI-Image-Editor
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ba2a0cbd5 | |||
| d4f9735f88 | |||
|
|
4b5b1a5eba | ||
|
|
eae15ced5a | ||
|
|
70684b2ddf | ||
|
|
9a5e4d8041 |
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,38 +86,17 @@ 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获取Blob,Blob可能已被清理');
|
||||||
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获取
|
throw new Error('无法从Blob 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获取图像数据');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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对象,使用唯一文件名
|
||||||
@@ -177,69 +168,70 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
|||||||
// 发送POST请求
|
// 发送POST请求
|
||||||
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('上传缓存已清除')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
|||||||
Reference in New Issue
Block a user