You've already forked Nano-Banana-AI-Image-Editor
新增 现在参考图可以拖动排序了;
修复 双参考图生成结果显示问题;
This commit is contained in:
10
src/App.tsx
10
src/App.tsx
@@ -40,6 +40,16 @@ function AppContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
// 组件卸载时清理所有Blob URL
|
||||||
|
return () => {
|
||||||
|
const { blobStore } = useAppStore.getState();
|
||||||
|
blobStore.forEach((blob, url) => {
|
||||||
|
if (url.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 在挂载时设置移动设备默认值
|
// 在挂载时设置移动设备默认值
|
||||||
|
|||||||
@@ -612,11 +612,20 @@ export const HistoryPanel: React.FC<{
|
|||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
selectGeneration(generation.id);
|
selectGeneration(generation.id);
|
||||||
// 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产
|
// 设置画布图像为生成结果图像,而不是参考图像
|
||||||
let imageUrl = null;
|
let imageUrl = null;
|
||||||
|
|
||||||
// 首先尝试获取参考图像
|
// 优先使用生成结果图像
|
||||||
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
|
if (generation.outputAssets && generation.outputAssets.length > 0) {
|
||||||
|
const asset = generation.outputAssets[0];
|
||||||
|
if (asset.url) {
|
||||||
|
const uploadedUrl = getUploadedImageUrl(generation, 0);
|
||||||
|
imageUrl = uploadedUrl || asset.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有生成结果图像,则使用参考图像
|
||||||
|
if (!imageUrl && generation.sourceAssets && generation.sourceAssets.length > 0) {
|
||||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||||
const outputAssetsCount = generation.outputAssets?.length || 0;
|
const outputAssetsCount = generation.outputAssets?.length || 0;
|
||||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||||
@@ -628,8 +637,57 @@ export const HistoryPanel: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有参考图像,则使用生成结果图像
|
if (imageUrl) {
|
||||||
if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) {
|
// 检查是否是Blob URL并且可能已经失效
|
||||||
|
if (imageUrl.startsWith('blob:')) {
|
||||||
|
// 预先检查Blob URL是否有效
|
||||||
|
fetch(imageUrl)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
// Blob URL失效,尝试从AppStore重新获取
|
||||||
|
const { getBlob } = useAppStore.getState();
|
||||||
|
const blob = getBlob(imageUrl);
|
||||||
|
if (blob) {
|
||||||
|
console.log('从AppStore找到Blob,重新创建URL...');
|
||||||
|
const newUrl = URL.createObjectURL(blob);
|
||||||
|
setCanvasImage(newUrl);
|
||||||
|
} else {
|
||||||
|
// 如果AppStore中也没有,直接设置原URL,让ImageCanvas处理
|
||||||
|
setCanvasImage(imageUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Blob URL有效,直接使用
|
||||||
|
setCanvasImage(imageUrl);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 网络错误,尝试从AppStore重新获取
|
||||||
|
const { getBlob } = useAppStore.getState();
|
||||||
|
const blob = getBlob(imageUrl);
|
||||||
|
if (blob) {
|
||||||
|
console.log('从AppStore找到Blob,重新创建URL...');
|
||||||
|
const newUrl = URL.createObjectURL(blob);
|
||||||
|
setCanvasImage(newUrl);
|
||||||
|
} else {
|
||||||
|
// 如果AppStore中也没有,直接设置原URL,让ImageCanvas处理
|
||||||
|
setCanvasImage(imageUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 非Blob URL直接设置
|
||||||
|
setCanvasImage(imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
// 设置当前悬停的记录
|
||||||
|
setHoveredRecord({type: 'generation', id: generation.id});
|
||||||
|
|
||||||
|
// 优先显示生成结果图像,如果没有生成结果图像则显示参考图像
|
||||||
|
let imageUrl = null;
|
||||||
|
|
||||||
|
// 优先使用生成结果图像
|
||||||
|
if (generation.outputAssets && generation.outputAssets.length > 0) {
|
||||||
const asset = generation.outputAssets[0];
|
const asset = generation.outputAssets[0];
|
||||||
if (asset.url) {
|
if (asset.url) {
|
||||||
const uploadedUrl = getUploadedImageUrl(generation, 0);
|
const uploadedUrl = getUploadedImageUrl(generation, 0);
|
||||||
@@ -637,19 +695,8 @@ export const HistoryPanel: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageUrl) {
|
// 如果没有生成结果图像,则使用参考图像
|
||||||
setCanvasImage(imageUrl);
|
if (!imageUrl && generation.sourceAssets && generation.sourceAssets.length > 0) {
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
// 设置当前悬停的记录
|
|
||||||
setHoveredRecord({type: 'generation', id: generation.id});
|
|
||||||
|
|
||||||
// 优先显示参考图像,如果没有参考图像则显示生成结果图像
|
|
||||||
let imageUrl = null;
|
|
||||||
|
|
||||||
// 首先尝试获取参考图像
|
|
||||||
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
|
|
||||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||||
const outputAssetsCount = generation.outputAssets?.length || 0;
|
const outputAssetsCount = generation.outputAssets?.length || 0;
|
||||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||||
@@ -657,12 +704,6 @@ export const HistoryPanel: React.FC<{
|
|||||||
(generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null);
|
(generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有参考图像,则使用生成结果图像
|
|
||||||
if (!imageUrl) {
|
|
||||||
imageUrl = getUploadedImageUrl(generation, 0) ||
|
|
||||||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
// 创建图像对象以获取尺寸
|
// 创建图像对象以获取尺寸
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -769,11 +810,20 @@ export const HistoryPanel: React.FC<{
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
// 优先显示参考图像,如果没有参考图像则显示生成结果图像
|
// 优先显示生成结果图像,如果没有生成结果图像则显示参考图像
|
||||||
let imageUrl = null;
|
let imageUrl = null;
|
||||||
|
|
||||||
// 首先尝试获取参考图像
|
// 优先使用生成结果图像
|
||||||
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
|
if (generation.outputAssets && generation.outputAssets.length > 0) {
|
||||||
|
const asset = generation.outputAssets[0];
|
||||||
|
if (asset.url) {
|
||||||
|
const uploadedUrl = getUploadedImageUrl(generation, 0);
|
||||||
|
imageUrl = uploadedUrl || asset.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有生成结果图像,则使用参考图像
|
||||||
|
if (!imageUrl && generation.sourceAssets && generation.sourceAssets.length > 0) {
|
||||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||||
const outputAssetsCount = generation.outputAssets?.length || 0;
|
const outputAssetsCount = generation.outputAssets?.length || 0;
|
||||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||||
@@ -781,12 +831,6 @@ export const HistoryPanel: React.FC<{
|
|||||||
(generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null);
|
(generation.sourceAssets[0].url ? generation.sourceAssets[0].url : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有参考图像,则使用生成结果图像
|
|
||||||
if (!imageUrl) {
|
|
||||||
imageUrl = getUploadedImageUrl(generation, 0) ||
|
|
||||||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
return <img src={imageUrl} alt="生成的变体" className="w-full h-full object-cover" />;
|
return <img src={imageUrl} alt="生成的变体" className="w-full h-full object-cover" />;
|
||||||
} else {
|
} else {
|
||||||
@@ -919,11 +963,20 @@ export const HistoryPanel: React.FC<{
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
selectEdit(edit.id);
|
selectEdit(edit.id);
|
||||||
selectGeneration(null);
|
selectGeneration(null);
|
||||||
// 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产
|
// 设置画布图像为编辑结果图像,而不是参考图像
|
||||||
let imageUrl = null;
|
let imageUrl = null;
|
||||||
|
|
||||||
// 首先尝试获取参考图像
|
// 优先使用编辑结果图像
|
||||||
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
|
if (edit.outputAssets && edit.outputAssets.length > 0) {
|
||||||
|
const asset = edit.outputAssets[0];
|
||||||
|
if (asset.url) {
|
||||||
|
const uploadedUrl = getUploadedImageUrl(edit, 0);
|
||||||
|
imageUrl = uploadedUrl || asset.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有编辑结果图像,则使用参考图像
|
||||||
|
if (!imageUrl && edit.sourceAssets && edit.sourceAssets.length > 0) {
|
||||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||||
const outputAssetsCount = edit.outputAssets?.length || 0;
|
const outputAssetsCount = edit.outputAssets?.length || 0;
|
||||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||||
@@ -935,8 +988,57 @@ export const HistoryPanel: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有参考图像,则使用编辑结果图像
|
if (imageUrl) {
|
||||||
if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) {
|
// 检查是否是Blob URL并且可能已经失效
|
||||||
|
if (imageUrl.startsWith('blob:')) {
|
||||||
|
// 预先检查Blob URL是否有效
|
||||||
|
fetch(imageUrl)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
// Blob URL失效,尝试从AppStore重新获取
|
||||||
|
const { getBlob } = useAppStore.getState();
|
||||||
|
const blob = getBlob(imageUrl);
|
||||||
|
if (blob) {
|
||||||
|
console.log('从AppStore找到Blob,重新创建URL...');
|
||||||
|
const newUrl = URL.createObjectURL(blob);
|
||||||
|
setCanvasImage(newUrl);
|
||||||
|
} else {
|
||||||
|
// 如果AppStore中也没有,直接设置原URL,让ImageCanvas处理
|
||||||
|
setCanvasImage(imageUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Blob URL有效,直接使用
|
||||||
|
setCanvasImage(imageUrl);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 网络错误,尝试从AppStore重新获取
|
||||||
|
const { getBlob } = useAppStore.getState();
|
||||||
|
const blob = getBlob(imageUrl);
|
||||||
|
if (blob) {
|
||||||
|
console.log('从AppStore找到Blob,重新创建URL...');
|
||||||
|
const newUrl = URL.createObjectURL(blob);
|
||||||
|
setCanvasImage(newUrl);
|
||||||
|
} else {
|
||||||
|
// 如果AppStore中也没有,直接设置原URL,让ImageCanvas处理
|
||||||
|
setCanvasImage(imageUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 非Blob URL直接设置
|
||||||
|
setCanvasImage(imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
// 设置当前悬停的记录
|
||||||
|
setHoveredRecord({type: 'edit', id: edit.id});
|
||||||
|
|
||||||
|
// 优先显示编辑结果图像,如果没有编辑结果图像则显示参考图像
|
||||||
|
let imageUrl = null;
|
||||||
|
|
||||||
|
// 优先使用编辑结果图像
|
||||||
|
if (edit.outputAssets && edit.outputAssets.length > 0) {
|
||||||
const asset = edit.outputAssets[0];
|
const asset = edit.outputAssets[0];
|
||||||
if (asset.url) {
|
if (asset.url) {
|
||||||
const uploadedUrl = getUploadedImageUrl(edit, 0);
|
const uploadedUrl = getUploadedImageUrl(edit, 0);
|
||||||
@@ -944,19 +1046,8 @@ export const HistoryPanel: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageUrl) {
|
// 如果没有编辑结果图像,则使用参考图像
|
||||||
setCanvasImage(imageUrl);
|
if (!imageUrl && edit.sourceAssets && edit.sourceAssets.length > 0) {
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
// 设置当前悬停的记录
|
|
||||||
setHoveredRecord({type: 'edit', id: edit.id});
|
|
||||||
|
|
||||||
// 优先显示参考图像,如果没有参考图像则显示编辑结果图像
|
|
||||||
let imageUrl = null;
|
|
||||||
|
|
||||||
// 首先尝试获取参考图像
|
|
||||||
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
|
|
||||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||||
const outputAssetsCount = edit.outputAssets?.length || 0;
|
const outputAssetsCount = edit.outputAssets?.length || 0;
|
||||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||||
@@ -964,12 +1055,6 @@ export const HistoryPanel: React.FC<{
|
|||||||
(edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null);
|
(edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有参考图像,则使用编辑结果图像
|
|
||||||
if (!imageUrl) {
|
|
||||||
imageUrl = getUploadedImageUrl(edit, 0) ||
|
|
||||||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
// 创建图像对象以获取尺寸
|
// 创建图像对象以获取尺寸
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
@@ -1016,11 +1101,20 @@ export const HistoryPanel: React.FC<{
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
// 优先显示参考图像,如果没有参考图像则显示编辑结果图像
|
// 优先显示编辑结果图像,如果没有编辑结果图像则显示参考图像
|
||||||
let imageUrl = null;
|
let imageUrl = null;
|
||||||
|
|
||||||
// 首先尝试获取参考图像
|
// 优先使用编辑结果图像
|
||||||
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
|
if (edit.outputAssets && edit.outputAssets.length > 0) {
|
||||||
|
const asset = edit.outputAssets[0];
|
||||||
|
if (asset.url) {
|
||||||
|
const uploadedUrl = getUploadedImageUrl(edit, 0);
|
||||||
|
imageUrl = uploadedUrl || asset.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有编辑结果图像,则使用参考图像
|
||||||
|
if (!imageUrl && edit.sourceAssets && edit.sourceAssets.length > 0) {
|
||||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||||
const outputAssetsCount = edit.outputAssets?.length || 0;
|
const outputAssetsCount = edit.outputAssets?.length || 0;
|
||||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||||
@@ -1028,12 +1122,6 @@ export const HistoryPanel: React.FC<{
|
|||||||
(edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null);
|
(edit.sourceAssets[0].url ? edit.sourceAssets[0].url : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有参考图像,则使用编辑结果图像
|
|
||||||
if (!imageUrl) {
|
|
||||||
imageUrl = getUploadedImageUrl(edit, 0) ||
|
|
||||||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
return <img src={imageUrl} alt="编辑的变体" className="w-full h-full object-cover" />;
|
return <img src={imageUrl} alt="编辑的变体" className="w-full h-full object-cover" />;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export const ImageCanvas: React.FC = () => {
|
|||||||
const {
|
const {
|
||||||
canvasImage,
|
canvasImage,
|
||||||
canvasZoom,
|
canvasZoom,
|
||||||
|
canvasPan,
|
||||||
setCanvasZoom,
|
setCanvasZoom,
|
||||||
|
setCanvasPan,
|
||||||
brushStrokes,
|
brushStrokes,
|
||||||
addBrushStroke,
|
addBrushStroke,
|
||||||
showMasks,
|
showMasks,
|
||||||
@@ -48,132 +50,194 @@ export const ImageCanvas: React.FC = () => {
|
|||||||
|
|
||||||
// 加载图像
|
// 加载图像
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let img: HTMLImageElement | null = null;
|
console.log('useEffect triggered, canvasImage:', canvasImage);
|
||||||
|
|
||||||
if (canvasImage) {
|
// 如果没有图像URL,直接返回
|
||||||
console.log('开始加载图像,URL:', canvasImage);
|
if (!canvasImage) {
|
||||||
|
console.log('没有图像需要加载');
|
||||||
|
setImage(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let img: HTMLImageElement | null = null;
|
||||||
|
let isCancelled = false;
|
||||||
|
|
||||||
|
console.log('开始加载图像,URL:', canvasImage);
|
||||||
|
|
||||||
|
img = new window.Image();
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
|
console.log('图像加载被取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
img = new window.Image();
|
console.log('图像加载成功,尺寸:', img.width, 'x', img.height);
|
||||||
const isCancelled = false;
|
setImage(img);
|
||||||
|
|
||||||
img.onload = () => {
|
// 只在图像首次加载时自动适应画布
|
||||||
// 检查是否已取消
|
if (!isCancelled && img) {
|
||||||
if (isCancelled) {
|
const isMobile = window.innerWidth < 768;
|
||||||
console.log('图像加载被取消');
|
const padding = isMobile ? 0.9 : 0.8;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('图像加载成功,尺寸:', img.width, 'x', img.height);
|
const scaleX = (stageSize.width * padding) / img.width;
|
||||||
setImage(img);
|
const scaleY = (stageSize.height * padding) / img.height;
|
||||||
|
|
||||||
// 只在图像首次加载时自动适应画布
|
const maxZoom = isMobile ? 0.3 : 0.8;
|
||||||
if (!isCancelled && img) {
|
const optimalZoom = Math.min(scaleX, scaleY, maxZoom);
|
||||||
const isMobile = window.innerWidth < 768;
|
|
||||||
const padding = isMobile ? 0.9 : 0.8;
|
// 立即更新React状态以确保Konva Image组件使用正确的缩放值
|
||||||
|
setCanvasZoom(optimalZoom);
|
||||||
|
setCanvasPan({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
// 使用setTimeout确保DOM已更新后再设置Stage
|
||||||
|
setTimeout(() => {
|
||||||
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const scaleX = (stageSize.width * padding) / img.width;
|
if (!isCancelled && img) {
|
||||||
const scaleY = (stageSize.height * padding) / img.height;
|
// 直接设置缩放,但保持Stage居中
|
||||||
|
const stage = stageRef.current;
|
||||||
const maxZoom = isMobile ? 0.3 : 0.8;
|
if (stage) {
|
||||||
const optimalZoom = Math.min(scaleX, scaleY, maxZoom);
|
stage.scale({ x: optimalZoom, y: optimalZoom });
|
||||||
|
// 重置Stage位置以确保居中
|
||||||
// 立即更新React状态以确保Konva Image组件使用正确的缩放值
|
stage.position({ x: 0, y: 0 });
|
||||||
setCanvasZoom(optimalZoom);
|
stage.batchDraw();
|
||||||
setCanvasPan({ x: 0, y: 0 });
|
}
|
||||||
|
|
||||||
// 使用setTimeout确保DOM已更新后再设置Stage
|
console.log('图像自动适应画布完成,缩放:', optimalZoom);
|
||||||
setTimeout(() => {
|
}
|
||||||
if (!isCancelled && img) {
|
}, 0);
|
||||||
// 直接设置缩放,但保持Stage居中
|
}
|
||||||
const stage = stageRef.current;
|
};
|
||||||
if (stage) {
|
|
||||||
stage.scale({ x: optimalZoom, y: optimalZoom });
|
img.onerror = (error) => {
|
||||||
// 重置Stage位置以确保居中
|
// 检查是否已取消
|
||||||
stage.position({ x: 0, y: 0 });
|
if (isCancelled) {
|
||||||
stage.batchDraw();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('图像加载失败:', error);
|
||||||
|
console.error('图像URL:', canvasImage);
|
||||||
|
|
||||||
|
// 检查是否是IndexedDB URL
|
||||||
|
if (canvasImage.startsWith('indexeddb://')) {
|
||||||
|
console.log('正在处理IndexedDB图像...');
|
||||||
|
|
||||||
|
// 从IndexedDB获取图像并创建Blob URL
|
||||||
|
const imageId = canvasImage.replace('indexeddb://', '');
|
||||||
|
import('../services/referenceImageService').then((module) => {
|
||||||
|
const referenceImageService = module;
|
||||||
|
referenceImageService.getReferenceImage(imageId)
|
||||||
|
.then(blob => {
|
||||||
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('图像自动适应画布完成,缩放:', optimalZoom);
|
if (blob) {
|
||||||
}
|
const newUrl = URL.createObjectURL(blob);
|
||||||
}, 0);
|
console.log('从IndexedDB创建新的Blob URL:', newUrl);
|
||||||
}
|
// 更新canvasImage为新的URL
|
||||||
};
|
import('../store/useAppStore').then((storeModule) => {
|
||||||
|
const useAppStore = storeModule.useAppStore;
|
||||||
img.onerror = (error) => {
|
// 检查是否已取消
|
||||||
if (!isCancelled) {
|
if (!isCancelled) {
|
||||||
console.error('图像加载失败:', error);
|
useAppStore.getState().setCanvasImage(newUrl);
|
||||||
console.error('图像URL:', canvasImage);
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('IndexedDB中未找到图像');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('从IndexedDB获取图像时出错:', err);
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否是Blob URL
|
console.error('导入referenceImageService时出错:', err);
|
||||||
if (canvasImage.startsWith('blob:')) {
|
});
|
||||||
console.log('正在检查Blob URL是否有效...');
|
}
|
||||||
|
// 检查是否是Blob URL
|
||||||
|
else if (canvasImage.startsWith('blob:')) {
|
||||||
|
console.log('正在检查Blob URL是否有效...');
|
||||||
|
|
||||||
|
// 尝试从AppStore重新获取Blob并创建新的URL
|
||||||
|
import('../store/useAppStore').then((module) => {
|
||||||
|
const useAppStore = module.useAppStore;
|
||||||
|
const blob = useAppStore.getState().getBlob(canvasImage);
|
||||||
|
if (blob) {
|
||||||
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查Blob URL是否仍然有效
|
console.log('从AppStore找到Blob,尝试重新创建URL...');
|
||||||
|
// 重新创建Blob URL并重试加载
|
||||||
|
const newUrl = URL.createObjectURL(blob);
|
||||||
|
console.log('创建新的Blob URL:', newUrl);
|
||||||
|
// 更新canvasImage为新的URL
|
||||||
|
useAppStore.getState().setCanvasImage(newUrl);
|
||||||
|
} else {
|
||||||
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('AppStore中未找到Blob');
|
||||||
|
// 如果AppStore中也没有,尝试通过fetch检查URL
|
||||||
fetch(canvasImage)
|
fetch(canvasImage)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Blob URL无法访问:', response.status, response.statusText);
|
console.error('Blob URL无法访问:', response.status, response.statusText);
|
||||||
// 尝试从AppStore重新获取Blob并创建新的URL
|
|
||||||
import('../store/useAppStore').then((module) => {
|
|
||||||
const useAppStore = module.useAppStore;
|
|
||||||
const blob = useAppStore.getState().getBlob(canvasImage);
|
|
||||||
if (blob) {
|
|
||||||
console.log('从AppStore找到Blob,尝试重新创建URL...');
|
|
||||||
// 重新创建Blob URL并重试加载
|
|
||||||
const newUrl = URL.createObjectURL(blob);
|
|
||||||
console.log('创建新的Blob URL:', newUrl);
|
|
||||||
// 更新canvasImage为新的URL
|
|
||||||
useAppStore.getState().setCanvasImage(newUrl);
|
|
||||||
} else {
|
|
||||||
console.error('AppStore中未找到Blob');
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('导入AppStore时出错:', err);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Blob URL可以访问');
|
console.log('Blob URL可以访问,但图像加载仍然失败');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(fetchErr => {
|
||||||
console.error('检查Blob URL时出错:', err);
|
// 检查是否已取消
|
||||||
// 尝试从AppStore重新获取Blob
|
if (isCancelled) {
|
||||||
import('../store/useAppStore').then((module) => {
|
return;
|
||||||
const useAppStore = module.useAppStore;
|
}
|
||||||
const blob = useAppStore.getState().getBlob(canvasImage);
|
|
||||||
if (blob) {
|
console.error('检查Blob URL时出错:', fetchErr);
|
||||||
console.log('从AppStore找到Blob,尝试重新创建URL...');
|
|
||||||
// 重新创建Blob URL并重试加载
|
|
||||||
const newUrl = URL.createObjectURL(blob);
|
|
||||||
console.log('创建新的Blob URL:', newUrl);
|
|
||||||
// 更新canvasImage为新的URL
|
|
||||||
useAppStore.getState().setCanvasImage(newUrl);
|
|
||||||
} else {
|
|
||||||
console.error('AppStore中未找到Blob');
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('导入AppStore时出错:', err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}).catch(err => {
|
||||||
};
|
// 检查是否已取消
|
||||||
|
if (isCancelled) {
|
||||||
img.src = canvasImage;
|
return;
|
||||||
} else {
|
}
|
||||||
console.log('没有图像需要加载');
|
|
||||||
// 当没有图像时,清理之前的图像对象
|
console.error('导入AppStore时出错:', err);
|
||||||
if (image) {
|
});
|
||||||
// 清理图像对象以释放内存
|
|
||||||
image.onload = null;
|
|
||||||
image.onerror = null;
|
|
||||||
image.src = '';
|
|
||||||
}
|
}
|
||||||
setImage(null);
|
};
|
||||||
}
|
|
||||||
|
img.src = canvasImage;
|
||||||
|
|
||||||
// 清理函数
|
// 清理函数
|
||||||
return () => {
|
return () => {
|
||||||
console.log('清理图像加载资源');
|
console.log('清理图像加载资源');
|
||||||
|
// 标记为已取消
|
||||||
|
isCancelled = true;
|
||||||
// 取消图像加载
|
// 取消图像加载
|
||||||
if (img) {
|
if (img) {
|
||||||
img.onload = null;
|
img.onload = null;
|
||||||
@@ -181,15 +245,8 @@ export const ImageCanvas: React.FC = () => {
|
|||||||
// 清理图像源以释放内存
|
// 清理图像源以释放内存
|
||||||
img.src = '';
|
img.src = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理之前的图像对象
|
|
||||||
if (image) {
|
|
||||||
image.onload = null;
|
|
||||||
image.onerror = null;
|
|
||||||
image.src = '';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [canvasImage, image, setCanvasZoom, stageSize.height, stageSize.width]); // 添加所有依赖项
|
}, [canvasImage, setCanvasZoom, setCanvasPan, stageSize.height, stageSize.width]); // 移除image依赖项
|
||||||
|
|
||||||
// 处理舞台大小调整
|
// 处理舞台大小调整
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ const ImagePreview: React.FC<{
|
|||||||
index: number;
|
index: number;
|
||||||
selectedTool: 'generate' | 'edit' | 'mask';
|
selectedTool: 'generate' | 'edit' | 'mask';
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
}> = ({ image, index, onRemove }) => {
|
onDragStart?: (e: React.DragEvent<HTMLDivElement>, index: number) => void;
|
||||||
|
onDragOver?: (e: React.DragEvent<HTMLDivElement>, index: number) => void;
|
||||||
|
onDragEnd?: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||||
|
onDrop?: (e: React.DragEvent<HTMLDivElement>, index: number) => void;
|
||||||
|
isDragging?: boolean;
|
||||||
|
}> = ({ image, index, onRemove, onDragStart, onDragOver, onDragEnd, onDrop, isDragging }) => {
|
||||||
const [imageSrc, setImageSrc] = useState<string>(image);
|
const [imageSrc, setImageSrc] = useState<string>(image);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
@@ -74,7 +79,20 @@ const ImagePreview: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div
|
||||||
|
className={cn("relative", isDragging ? "opacity-50" : "")}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => onDragStart && onDragStart(e, index)}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onDragOver && onDragOver(e, index);
|
||||||
|
}}
|
||||||
|
onDragEnd={(e) => onDragEnd && onDragEnd(e)}
|
||||||
|
onDrop={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onDrop && onDrop(e, index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={`参考图像 ${index + 1}`}
|
alt={`参考图像 ${index + 1}`}
|
||||||
@@ -111,6 +129,7 @@ export const PromptComposer: React.FC = () => {
|
|||||||
uploadedImages,
|
uploadedImages,
|
||||||
addUploadedImage,
|
addUploadedImage,
|
||||||
removeUploadedImage,
|
removeUploadedImage,
|
||||||
|
reorderUploadedImage,
|
||||||
clearUploadedImages,
|
clearUploadedImages,
|
||||||
editReferenceImages,
|
editReferenceImages,
|
||||||
addEditReferenceImage,
|
addEditReferenceImage,
|
||||||
@@ -130,6 +149,7 @@ export const PromptComposer: React.FC = () => {
|
|||||||
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
||||||
const [showHintsModal, setShowHintsModal] = useState(false);
|
const [showHintsModal, setShowHintsModal] = useState(false);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// 初始化参考图像数据库
|
// 初始化参考图像数据库
|
||||||
@@ -237,6 +257,13 @@ export const PromptComposer: React.FC = () => {
|
|||||||
// 创建一个特殊的URL来标识这是存储在IndexedDB中的图像
|
// 创建一个特殊的URL来标识这是存储在IndexedDB中的图像
|
||||||
const imageUrl = `indexeddb://${imageId}`;
|
const imageUrl = `indexeddb://${imageId}`;
|
||||||
|
|
||||||
|
// 同时创建一个可以直接在画布上显示的Blob URL
|
||||||
|
const blob = await referenceImageService.getReferenceImage(imageId);
|
||||||
|
let displayUrl = imageUrl; // 默认使用IndexedDB URL
|
||||||
|
if (blob) {
|
||||||
|
displayUrl = URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedTool === 'generate') {
|
if (selectedTool === 'generate') {
|
||||||
// 添加到参考图像(最多2张)
|
// 添加到参考图像(最多2张)
|
||||||
if (uploadedImages.length < 2) {
|
if (uploadedImages.length < 2) {
|
||||||
@@ -247,15 +274,15 @@ export const PromptComposer: React.FC = () => {
|
|||||||
if (editReferenceImages.length < 2) {
|
if (editReferenceImages.length < 2) {
|
||||||
addEditReferenceImage(imageUrl);
|
addEditReferenceImage(imageUrl);
|
||||||
}
|
}
|
||||||
// 如果没有画布图像,则设置为画布图像
|
// 如果没有画布图像,则设置为画布图像(使用可以直接显示的URL)
|
||||||
if (!canvasImage) {
|
if (!canvasImage) {
|
||||||
setCanvasImage(imageUrl);
|
setCanvasImage(displayUrl);
|
||||||
}
|
}
|
||||||
} else if (selectedTool === 'mask') {
|
} else if (selectedTool === 'mask') {
|
||||||
// 遮罩模式下,将图像添加为参考图像而不是清除现有图像
|
// 遮罩模式下,将图像添加为参考图像而不是清除现有图像
|
||||||
// 只有在没有画布图像时才设置为画布图像
|
// 只有在没有画布图像时才设置为画布图像(使用可以直接显示的URL)
|
||||||
if (!canvasImage) {
|
if (!canvasImage) {
|
||||||
setCanvasImage(imageUrl);
|
setCanvasImage(displayUrl);
|
||||||
}
|
}
|
||||||
// 不清除现有的上传图像,而是将新图像添加为参考图像(如果还有空间)
|
// 不清除现有的上传图像,而是将新图像添加为参考图像(如果还有空间)
|
||||||
if (uploadedImages.length < 2) {
|
if (uploadedImages.length < 2) {
|
||||||
@@ -294,6 +321,31 @@ export const PromptComposer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 拖拽排序处理函数
|
||||||
|
const handleDragStart = (e: React.DragEvent<HTMLDivElement>, index: number) => {
|
||||||
|
setDraggedIndex(index);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
// 在Firefox中需要设置dataTransfer数据
|
||||||
|
e.dataTransfer.setData('text/plain', index.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOverPreview = (e: React.DragEvent<HTMLDivElement>, index: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDropPreview = (e: React.DragEvent<HTMLDivElement>, index: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (draggedIndex !== null && draggedIndex !== index) {
|
||||||
|
reorderUploadedImage(draggedIndex, index);
|
||||||
|
}
|
||||||
|
setDraggedIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
setDraggedIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleClearSession = async () => {
|
const handleClearSession = async () => {
|
||||||
setCurrentPrompt('');
|
setCurrentPrompt('');
|
||||||
clearUploadedImages();
|
clearUploadedImages();
|
||||||
@@ -311,6 +363,9 @@ export const PromptComposer: React.FC = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清空IndexedDB中的参考图像失败:', error);
|
console.error('清空IndexedDB中的参考图像失败:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理所有Blob URL
|
||||||
|
useAppStore.getState().cleanupAllBlobUrls();
|
||||||
};
|
};
|
||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
@@ -461,6 +516,11 @@ export const PromptComposer: React.FC = () => {
|
|||||||
index={index}
|
index={index}
|
||||||
selectedTool={selectedTool}
|
selectedTool={selectedTool}
|
||||||
onRemove={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
|
onRemove={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
|
||||||
|
onDragStart={selectedTool === 'generate' ? handleDragStart : undefined}
|
||||||
|
onDragOver={selectedTool === 'generate' ? handleDragOverPreview : undefined}
|
||||||
|
onDragEnd={selectedTool === 'generate' ? handleDragEnd : undefined}
|
||||||
|
onDrop={selectedTool === 'generate' ? handleDropPreview : undefined}
|
||||||
|
isDragging={selectedTool === 'generate' && draggedIndex === index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -208,7 +208,15 @@ export const useImageGeneration = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
addGeneration(generation);
|
addGeneration(generation);
|
||||||
setCanvasImage(outputAssets[0].url);
|
|
||||||
|
// 调试日志:检查outputAssets
|
||||||
|
console.log('生成完成,outputAssets:', outputAssets);
|
||||||
|
if (outputAssets && outputAssets.length > 0) {
|
||||||
|
console.log('第一个输出资产URL:', outputAssets[0].url);
|
||||||
|
setCanvasImage(outputAssets[0].url);
|
||||||
|
} else {
|
||||||
|
console.error('生成完成但没有输出资产');
|
||||||
|
}
|
||||||
|
|
||||||
// 自动选择新生成的记录
|
// 自动选择新生成的记录
|
||||||
const { selectGeneration } = useAppStore.getState();
|
const { selectGeneration } = useAppStore.getState();
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ interface AppState {
|
|||||||
|
|
||||||
addUploadedImage: (url: string) => void;
|
addUploadedImage: (url: string) => void;
|
||||||
removeUploadedImage: (index: number) => void;
|
removeUploadedImage: (index: number) => void;
|
||||||
|
reorderUploadedImage: (fromIndex: number, toIndex: number) => void;
|
||||||
clearUploadedImages: () => void;
|
clearUploadedImages: () => void;
|
||||||
|
|
||||||
addEditReferenceImage: (url: string) => void;
|
addEditReferenceImage: (url: string) => void;
|
||||||
@@ -194,6 +195,12 @@ export const useAppStore = create<AppState>()(
|
|||||||
uploadedImages: state.uploadedImages.filter((_, i) => i !== index)
|
uploadedImages: state.uploadedImages.filter((_, i) => i !== index)
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
reorderUploadedImage: (fromIndex, toIndex) => set((state) => {
|
||||||
|
const newUploadedImages = [...state.uploadedImages];
|
||||||
|
const [movedItem] = newUploadedImages.splice(fromIndex, 1);
|
||||||
|
newUploadedImages.splice(toIndex, 0, movedItem);
|
||||||
|
return { uploadedImages: newUploadedImages };
|
||||||
|
}),
|
||||||
clearUploadedImages: () => set((state) => {
|
clearUploadedImages: () => set((state) => {
|
||||||
// 删除所有存储在IndexedDB中的参考图像
|
// 删除所有存储在IndexedDB中的参考图像
|
||||||
state.uploadedImages.forEach(imageUrl => {
|
state.uploadedImages.forEach(imageUrl => {
|
||||||
@@ -557,19 +564,27 @@ export const useAppStore = create<AppState>()(
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
// 释放指定的Blob URLs
|
// 释放指定的Blob URLs
|
||||||
revokeBlobUrls: () => set((state) => {
|
revokeBlobUrls: (urls: string[]) => set((state) => {
|
||||||
// 不再自动清理Blob URL,以确保参考图像不会被意外删除
|
// 清理指定的Blob URL
|
||||||
// 只有在用户明确请求清除会话时才清理
|
const newBlobStore = new Map(state.blobStore);
|
||||||
console.log('Blob清理已禁用,参考图像将被永久保留');
|
urls.forEach(url => {
|
||||||
return state;
|
if (url.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
newBlobStore.delete(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { blobStore: newBlobStore };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 释放所有Blob URLs
|
// 释放所有Blob URLs
|
||||||
cleanupAllBlobUrls: () => set((state) => {
|
cleanupAllBlobUrls: () => set((state) => {
|
||||||
// 不再自动清理Blob URL,以确保参考图像不会被意外删除
|
// 清理所有Blob URL
|
||||||
// 只有在用户明确请求清除会话时才清理
|
state.blobStore.forEach((blob, url) => {
|
||||||
console.log('Blob清理已禁用,参考图像将被永久保留');
|
if (url.startsWith('blob:')) {
|
||||||
return state;
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { blobStore: new Map() };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 定期清理Blob URL
|
// 定期清理Blob URL
|
||||||
|
|||||||
Reference in New Issue
Block a user