You've already forked Nano-Banana-AI-Image-Editor
阶段性提交
This commit is contained in:
@@ -4,7 +4,6 @@ import { Button } from './ui/Button';
|
||||
import { History, Download, Image as ImageIcon, Trash2 } from 'lucide-react';
|
||||
import { cn } from '../utils/cn';
|
||||
import { ImagePreviewModal } from './ImagePreviewModal';
|
||||
import * as indexedDBService from '../services/indexedDBService';
|
||||
import { useIndexedDBListener } from '../hooks/useIndexedDBListener';
|
||||
import { DayPicker } from 'react-day-picker';
|
||||
import zhCN from 'react-day-picker/dist/locale/zh-CN';
|
||||
@@ -13,9 +12,8 @@ export const HistoryPanel: React.FC<{
|
||||
setHoveredImage: (image: {url: string, title: string, width?: number, height?: number} | null) => void,
|
||||
setPreviewPosition?: (position: {x: number, y: number} | null) => void
|
||||
}> = ({ setHoveredImage, setPreviewPosition }) => {
|
||||
const {
|
||||
const {
|
||||
currentProject,
|
||||
canvasImage,
|
||||
selectedGenerationId,
|
||||
selectedEditId,
|
||||
selectGeneration,
|
||||
@@ -23,7 +21,6 @@ export const HistoryPanel: React.FC<{
|
||||
showHistory,
|
||||
setShowHistory,
|
||||
setCanvasImage,
|
||||
selectedTool,
|
||||
removeGeneration,
|
||||
removeEdit
|
||||
} = useAppStore();
|
||||
@@ -68,18 +65,28 @@ export const HistoryPanel: React.FC<{
|
||||
const [startDate, setStartDate] = useState<string>(() => {
|
||||
// 初始化时默认显示今天的记录
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today.toISOString().split('T')[0];
|
||||
});
|
||||
const [endDate, setEndDate] = useState<string>(() => {
|
||||
// 初始化时默认显示今天的记录
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today.toISOString().split('T')[0];
|
||||
});
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [showDatePicker, setShowDatePicker] = useState(false); // 控制日期选择器的显示
|
||||
const [dateRange, setDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({
|
||||
from: new Date(new Date().setHours(0, 0, 0, 0)),
|
||||
to: new Date(new Date().setHours(0, 0, 0, 0))
|
||||
from: (() => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today;
|
||||
})(),
|
||||
to: (() => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today;
|
||||
})()
|
||||
});
|
||||
|
||||
// 分页状态
|
||||
@@ -87,17 +94,18 @@ export const HistoryPanel: React.FC<{
|
||||
const itemsPerPage = 20; // 减少每页显示的项目数
|
||||
|
||||
// 获取当前图像尺寸
|
||||
const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null);
|
||||
// const [imageDimensions, setImageDimensions] = React.useState<{ width: number; height: number } | null>(null);
|
||||
|
||||
// 当currentProject为空时,使用dbGenerations和dbEdits作为备选
|
||||
const displayGenerations = currentProject ? currentProject.generations : dbGenerations;
|
||||
const displayEdits = currentProject ? currentProject.edits : dbEdits;
|
||||
|
||||
// 筛选记录的函数
|
||||
const filterRecords = useCallback((records: any[], isGeneration: boolean) => {
|
||||
const filterRecords = useCallback((records: Array<{timestamp: number, prompt?: string, instruction?: string}>, isGeneration: boolean) => {
|
||||
return records.filter(record => {
|
||||
// 日期筛选 - 检查记录日期是否在筛选范围内
|
||||
const recordDate = new Date(record.timestamp);
|
||||
recordDate.setHours(0, 0, 0, 0); // 设置为当天的开始
|
||||
const recordDateStr = recordDate.toISOString().split('T')[0]; // 获取日期部分 YYYY-MM-DD
|
||||
|
||||
// 检查是否在日期范围内
|
||||
@@ -108,10 +116,10 @@ export const HistoryPanel: React.FC<{
|
||||
if (searchTerm) {
|
||||
if (isGeneration) {
|
||||
// 生成记录按提示词搜索
|
||||
return record.prompt.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
return record.prompt?.toLowerCase().includes(searchTerm.toLowerCase()) || false;
|
||||
} else {
|
||||
// 编辑记录按指令搜索
|
||||
return record.instruction.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
return record.instruction?.toLowerCase().includes(searchTerm.toLowerCase()) || false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,17 +131,17 @@ export const HistoryPanel: React.FC<{
|
||||
const filteredGenerations = useMemo(() => filterRecords(displayGenerations, true), [displayGenerations, filterRecords]);
|
||||
const filteredEdits = useMemo(() => filterRecords(displayEdits, false), [displayEdits, filterRecords]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (canvasImage) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImageDimensions({ width: img.width, height: img.height });
|
||||
};
|
||||
img.src = canvasImage;
|
||||
} else {
|
||||
setImageDimensions(null);
|
||||
}
|
||||
}, [canvasImage]);
|
||||
// React.useEffect(() => {
|
||||
// if (canvasImage) {
|
||||
// const img = new Image();
|
||||
// img.onload = () => {
|
||||
// setImageDimensions({ width: img.width, height: img.height });
|
||||
// };
|
||||
// img.src = canvasImage;
|
||||
// } else {
|
||||
// setImageDimensions(null);
|
||||
// }
|
||||
// }, [canvasImage]);
|
||||
|
||||
// 当项目变化时,解码Blob图像
|
||||
useEffect(() => {
|
||||
@@ -196,8 +204,8 @@ export const HistoryPanel: React.FC<{
|
||||
if (generationOrEdit.uploadResults && generationOrEdit.uploadResults[index]) {
|
||||
const uploadResult = generationOrEdit.uploadResults[index];
|
||||
if (uploadResult.success && uploadResult.url) {
|
||||
// 添加参数以降低图片质量
|
||||
return `${uploadResult.url}?x-oss-process=image/quality,q_30`; // 降低质量到30%
|
||||
// 返回原始链接,不添加任何参数
|
||||
return uploadResult.url;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -469,6 +477,13 @@ export const HistoryPanel: React.FC<{
|
||||
selected={dateRange}
|
||||
onSelect={(range) => {
|
||||
if (range) {
|
||||
// 确保日期时间设置为当天的开始
|
||||
if (range.from) {
|
||||
range.from.setHours(0, 0, 0, 0);
|
||||
}
|
||||
if (range.to) {
|
||||
range.to.setHours(0, 0, 0, 0);
|
||||
}
|
||||
setDateRange(range);
|
||||
// 更新字符串格式的日期用于筛选
|
||||
if (range.from) {
|
||||
@@ -511,7 +526,8 @@ export const HistoryPanel: React.FC<{
|
||||
className="text-xs p-1.5 rounded-l-none h-7 border-gray-200 text-gray-600 hover:bg-gray-100 card"
|
||||
onClick={() => {
|
||||
// 重置为显示今天的记录
|
||||
const today = new Date(new Date().setHours(0, 0, 0, 0));
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayStr = today.toISOString().split('T')[0];
|
||||
setStartDate(todayStr);
|
||||
setEndDate(todayStr);
|
||||
@@ -581,7 +597,7 @@ export const HistoryPanel: React.FC<{
|
||||
sortedGenerations.some(gen => gen.id === record.id)
|
||||
);
|
||||
|
||||
return paginatedGenerations.map((generation, index) => {
|
||||
return paginatedGenerations.map((generation: {id: string, sourceAssets?: Array<{url: string}>, outputAssets?: Array<{url: string}>}) => {
|
||||
// 计算全局索引用于显示编号
|
||||
const globalIndex = allRecords.findIndex(record => record.id === generation.id);
|
||||
|
||||
@@ -596,23 +612,57 @@ export const HistoryPanel: React.FC<{
|
||||
)}
|
||||
onClick={() => {
|
||||
selectGeneration(generation.id);
|
||||
// 设置画布图像为第一个输出资产
|
||||
if (generation.outputAssets && generation.outputAssets.length > 0) {
|
||||
// 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产
|
||||
let imageUrl = null;
|
||||
|
||||
// 首先尝试获取参考图像
|
||||
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = generation.outputAssets?.length || 0;
|
||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||
const uploadedUrl = getUploadedImageUrl(generation, uploadResultIndex);
|
||||
if (uploadedUrl) {
|
||||
imageUrl = uploadedUrl;
|
||||
} else if (generation.sourceAssets[0].url) {
|
||||
imageUrl = generation.sourceAssets[0].url;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有参考图像,则使用生成结果图像
|
||||
if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) {
|
||||
const asset = generation.outputAssets[0];
|
||||
if (asset.url) {
|
||||
setCanvasImage(asset.url);
|
||||
const uploadedUrl = getUploadedImageUrl(generation, 0);
|
||||
imageUrl = uploadedUrl || asset.url;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
setCanvasImage(imageUrl);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
// 设置当前悬停的记录
|
||||
setHoveredRecord({type: 'generation', id: generation.id});
|
||||
|
||||
// 优先使用上传后的远程链接,如果没有则使用原始链接
|
||||
let imageUrl = getUploadedImageUrl(generation, 0);
|
||||
if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) {
|
||||
imageUrl = generation.outputAssets[0].url;
|
||||
}
|
||||
// 优先显示参考图像,如果没有参考图像则显示生成结果图像
|
||||
let imageUrl = null;
|
||||
|
||||
// 首先尝试获取参考图像
|
||||
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = generation.outputAssets?.length || 0;
|
||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||
imageUrl = getUploadedImageUrl(generation, uploadResultIndex) ||
|
||||
(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) {
|
||||
// 创建图像对象以获取尺寸
|
||||
const img = new Image();
|
||||
@@ -719,9 +769,23 @@ export const HistoryPanel: React.FC<{
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
// 优先使用上传后的远程链接
|
||||
const imageUrl = getUploadedImageUrl(generation, 0) ||
|
||||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
|
||||
// 优先显示参考图像,如果没有参考图像则显示生成结果图像
|
||||
let imageUrl = null;
|
||||
|
||||
// 首先尝试获取参考图像
|
||||
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = generation.outputAssets?.length || 0;
|
||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||
imageUrl = getUploadedImageUrl(generation, uploadResultIndex) ||
|
||||
(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) {
|
||||
return <img src={imageUrl} alt="生成的变体" className="w-full h-full object-cover" />;
|
||||
@@ -748,12 +812,34 @@ export const HistoryPanel: React.FC<{
|
||||
className="h-8 w-8 bg-white/90 hover:bg-white text-gray-700 rounded-full shadow-md"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// 下载图像
|
||||
const imageUrl = getUploadedImageUrl(generation, 0) ||
|
||||
(generation.outputAssets && generation.outputAssets.length > 0 && generation.outputAssets[0].url ? generation.outputAssets[0].url : null);
|
||||
// 下载图像 - 优先下载参考图像,如果没有则下载生成结果图像
|
||||
let imageUrl = null;
|
||||
|
||||
// 首先尝试获取参考图像
|
||||
if (generation.sourceAssets && generation.sourceAssets.length > 0) {
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = generation.outputAssets?.length || 0;
|
||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||
imageUrl = getUploadedImageUrl(generation, uploadResultIndex) ||
|
||||
(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) {
|
||||
// 使用Promise来处理异步操作
|
||||
fetch(imageUrl)
|
||||
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
|
||||
// 添加更多缓存控制头以绕过CDN缓存
|
||||
fetch(imageUrl, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
})
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -767,6 +853,14 @@ export const HistoryPanel: React.FC<{
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('下载图像失败:', error);
|
||||
// 如果fetch失败,回退到直接使用a标签
|
||||
const link = document.createElement('a');
|
||||
link.href = imageUrl;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -809,7 +903,7 @@ export const HistoryPanel: React.FC<{
|
||||
sortedEdits.some(edit => edit.id === record.id)
|
||||
);
|
||||
|
||||
return paginatedEdits.map((edit, index) => {
|
||||
return paginatedEdits.map((edit: {id: string, sourceAssets?: Array<{url: string}>, outputAssets?: Array<{url: string}>}) => {
|
||||
// 计算全局索引用于显示编号
|
||||
const globalIndex = allRecords.findIndex(record => record.id === edit.id);
|
||||
|
||||
@@ -825,22 +919,55 @@ export const HistoryPanel: React.FC<{
|
||||
onClick={() => {
|
||||
selectEdit(edit.id);
|
||||
selectGeneration(null);
|
||||
// 设置画布图像为第一个输出资产
|
||||
if (edit.outputAssets && edit.outputAssets.length > 0) {
|
||||
// 设置画布图像为参考图像,如果没有参考图像则使用第一个输出资产
|
||||
let imageUrl = null;
|
||||
|
||||
// 首先尝试获取参考图像
|
||||
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = edit.outputAssets?.length || 0;
|
||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||
const uploadedUrl = getUploadedImageUrl(edit, uploadResultIndex);
|
||||
if (uploadedUrl) {
|
||||
imageUrl = uploadedUrl;
|
||||
} else if (edit.sourceAssets[0].url) {
|
||||
imageUrl = edit.sourceAssets[0].url;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有参考图像,则使用编辑结果图像
|
||||
if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) {
|
||||
const asset = edit.outputAssets[0];
|
||||
if (asset.url) {
|
||||
setCanvasImage(asset.url);
|
||||
const uploadedUrl = getUploadedImageUrl(edit, 0);
|
||||
imageUrl = uploadedUrl || asset.url;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
setCanvasImage(imageUrl);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
// 设置当前悬停的记录
|
||||
setHoveredRecord({type: 'edit', id: edit.id});
|
||||
|
||||
// 优先使用上传后的远程链接,如果没有则使用原始链接
|
||||
let imageUrl = getUploadedImageUrl(edit, 0);
|
||||
if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) {
|
||||
imageUrl = edit.outputAssets[0].url;
|
||||
// 优先显示参考图像,如果没有参考图像则显示编辑结果图像
|
||||
let imageUrl = null;
|
||||
|
||||
// 首先尝试获取参考图像
|
||||
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = edit.outputAssets?.length || 0;
|
||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||
imageUrl = getUploadedImageUrl(edit, uploadResultIndex) ||
|
||||
(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) {
|
||||
@@ -889,9 +1016,23 @@ export const HistoryPanel: React.FC<{
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
// 优先使用上传后的远程链接
|
||||
const imageUrl = getUploadedImageUrl(edit, 0) ||
|
||||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
|
||||
// 优先显示参考图像,如果没有参考图像则显示编辑结果图像
|
||||
let imageUrl = null;
|
||||
|
||||
// 首先尝试获取参考图像
|
||||
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = edit.outputAssets?.length || 0;
|
||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||
imageUrl = getUploadedImageUrl(edit, uploadResultIndex) ||
|
||||
(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) {
|
||||
return <img src={imageUrl} alt="编辑的变体" className="w-full h-full object-cover" />;
|
||||
@@ -918,12 +1059,34 @@ export const HistoryPanel: React.FC<{
|
||||
className="h-8 w-8 bg-white/90 hover:bg-white text-gray-700 rounded-full shadow-md"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// 下载图像
|
||||
const imageUrl = getUploadedImageUrl(edit, 0) ||
|
||||
(edit.outputAssets && edit.outputAssets.length > 0 && edit.outputAssets[0].url ? edit.outputAssets[0].url : null);
|
||||
// 下载图像 - 优先下载参考图像,如果没有则下载编辑结果图像
|
||||
let imageUrl = null;
|
||||
|
||||
// 首先尝试获取参考图像
|
||||
if (edit.sourceAssets && edit.sourceAssets.length > 0) {
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = edit.outputAssets?.length || 0;
|
||||
const uploadResultIndex = outputAssetsCount; // 第一个参考图像
|
||||
imageUrl = getUploadedImageUrl(edit, uploadResultIndex) ||
|
||||
(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) {
|
||||
// 使用Promise来处理异步操作
|
||||
fetch(imageUrl)
|
||||
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
|
||||
// 添加更多缓存控制头以绕过CDN缓存
|
||||
fetch(imageUrl, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
})
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -937,6 +1100,14 @@ export const HistoryPanel: React.FC<{
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('下载图像失败:', error);
|
||||
// 如果fetch失败,回退到直接使用a标签
|
||||
const link = document.createElement('a');
|
||||
link.href = imageUrl;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -1080,7 +1251,7 @@ export const HistoryPanel: React.FC<{
|
||||
{gen.outputAssets.length} 个生成结果
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{gen.outputAssets.slice(0, 4).map((asset: any, index: number) => {
|
||||
{gen.outputAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
|
||||
// 获取上传后的远程链接(如果存在)
|
||||
const uploadedUrl = gen.uploadResults && gen.uploadResults[index] && gen.uploadResults[index].success
|
||||
? `${gen.uploadResults[index].url}?x-oss-process=image/quality,q_30`
|
||||
@@ -1169,20 +1340,14 @@ export const HistoryPanel: React.FC<{
|
||||
{gen.sourceAssets.length} 个参考图像
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => {
|
||||
// 获取上传后的远程链接(如果存在)
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
// 但由于gen可能是轻量级记录,我们需要从dbGenerations中获取完整的记录
|
||||
const fullGen = dbGenerations.find(item => item.id === gen.id) || gen;
|
||||
const outputAssetsCount = fullGen.outputAssets?.length || 0;
|
||||
|
||||
// 确保索引在有效范围内
|
||||
const uploadResultIndex = outputAssetsCount + index;
|
||||
const uploadedUrl = fullGen.uploadResults && fullGen.uploadResults[uploadResultIndex] && fullGen.uploadResults[uploadResultIndex].success
|
||||
? `${fullGen.uploadResults[uploadResultIndex].url}?x-oss-process=image/quality,q_30`
|
||||
{gen.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
|
||||
// 优先使用上传后的远程链接,如果没有则使用asset中的URL
|
||||
// 参考图像在uploadResults中从索引1开始(图像2字段)
|
||||
const uploadResultIndex = 1 + index;
|
||||
const uploadedUrl = gen.uploadResults && gen.uploadResults[uploadResultIndex] && gen.uploadResults[uploadResultIndex].success
|
||||
? gen.uploadResults[uploadResultIndex].url
|
||||
: null;
|
||||
|
||||
// 如果没有上传的URL,则使用asset中的URL
|
||||
const displayUrl = uploadedUrl || asset.url || asset.blobUrl;
|
||||
|
||||
// 如果URL是blob:开头但已失效,尝试重新创建
|
||||
@@ -1320,10 +1485,10 @@ export const HistoryPanel: React.FC<{
|
||||
{selectedEdit.outputAssets.length} 个编辑结果
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedEdit.outputAssets.slice(0, 4).map((asset: any, index: number) => {
|
||||
{selectedEdit.outputAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
|
||||
// 获取上传后的远程链接(如果存在)
|
||||
const uploadedUrl = selectedEdit.uploadResults && selectedEdit.uploadResults[index] && selectedEdit.uploadResults[index].success
|
||||
? `${selectedEdit.uploadResults[index].url}?x-oss-process=image/quality,q_30`
|
||||
? selectedEdit.uploadResults[index].url
|
||||
: null;
|
||||
|
||||
// 如果没有上传的URL,则使用asset中的URL
|
||||
@@ -1401,6 +1566,96 @@ export const HistoryPanel: React.FC<{
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑参考图像 */}
|
||||
{selectedEdit.sourceAssets && selectedEdit.sourceAssets.length > 0 && (
|
||||
<div className="pt-2 border-t border-gray-200">
|
||||
<h5 className="text-xs font-medium text-gray-500 mb-2">编辑参考图像</h5>
|
||||
<div className="text-xs text-gray-600 mb-2">
|
||||
{selectedEdit.sourceAssets.length} 个参考图像
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedEdit.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
|
||||
// 优先使用上传后的远程链接,如果没有则使用asset中的URL
|
||||
// 参考图像在uploadResults中从索引1开始(图像2字段)
|
||||
const uploadResultIndex = 1 + index;
|
||||
const uploadedUrl = selectedEdit.uploadResults && selectedEdit.uploadResults[uploadResultIndex] && selectedEdit.uploadResults[uploadResultIndex].success
|
||||
? selectedEdit.uploadResults[uploadResultIndex].url
|
||||
: null;
|
||||
|
||||
const displayUrl = uploadedUrl || asset.url || asset.blobUrl;
|
||||
|
||||
// 如果URL是blob:开头但已失效,尝试重新创建
|
||||
if (displayUrl && displayUrl.startsWith('blob:')) {
|
||||
// 检查blob是否仍然有效
|
||||
const img = new Image();
|
||||
img.onerror = () => {
|
||||
// Blob URL可能已失效,尝试重新创建
|
||||
import('../store/useAppStore').then((module) => {
|
||||
const useAppStore = module.useAppStore;
|
||||
const blob = useAppStore.getState().getBlob(displayUrl);
|
||||
if (blob) {
|
||||
const newUrl = URL.createObjectURL(blob);
|
||||
// 更新显示
|
||||
const imgElement = document.querySelector(`img[src="${displayUrl}"]`);
|
||||
if (imgElement) {
|
||||
imgElement.src = newUrl;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
img.src = displayUrl;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={asset.id}
|
||||
className="relative w-16 h-16 rounded border overflow-hidden cursor-pointer hover:ring-2 hover:ring-yellow-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setPreviewModal({
|
||||
open: true,
|
||||
imageUrl: displayUrl,
|
||||
title: `编辑参考图像 ${index + 1}`,
|
||||
description: `${asset.width} × ${asset.height}`
|
||||
});
|
||||
}}
|
||||
>
|
||||
{displayUrl ? (
|
||||
<img
|
||||
src={displayUrl}
|
||||
alt={`编辑参考图像 ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
// 如果图像加载失败,尝试重新创建Blob URL
|
||||
if (displayUrl.startsWith('blob:')) {
|
||||
import('../store/useAppStore').then((module) => {
|
||||
const useAppStore = module.useAppStore;
|
||||
const blob = useAppStore.getState().getBlob(displayUrl);
|
||||
if (blob) {
|
||||
const newUrl = URL.createObjectURL(blob);
|
||||
(e.target as HTMLImageElement).src = newUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full bg-gray-100 flex items-center justify-center">
|
||||
<ImageIcon className="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{selectedEdit.sourceAssets.length > 4 && (
|
||||
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
|
||||
+{selectedEdit.sourceAssets.length - 4}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 原始生成参考 */}
|
||||
{parentGen && (
|
||||
<div className="pt-2 border-t border-gray-200">
|
||||
@@ -1415,18 +1670,14 @@ export const HistoryPanel: React.FC<{
|
||||
原始参考图像:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => {
|
||||
// 获取上传后的远程链接(如果存在)
|
||||
// 参考图像在uploadResults中从索引outputAssets.length开始
|
||||
const outputAssetsCount = parentGen.outputAssets?.length || 0;
|
||||
|
||||
// 确保索引在有效范围内
|
||||
const uploadResultIndex = outputAssetsCount + index;
|
||||
{parentGen.sourceAssets.slice(0, 4).map((asset: {id: string, url?: string, blobUrl?: string, width: number, height: number}, index: number) => {
|
||||
// 优先使用上传后的远程链接,如果没有则使用asset中的URL
|
||||
// 参考图像在uploadResults中从索引1开始(图像2字段)
|
||||
const uploadResultIndex = 1 + index;
|
||||
const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[uploadResultIndex] && parentGen.uploadResults[uploadResultIndex].success
|
||||
? `${parentGen.uploadResults[uploadResultIndex].url}?x-oss-process=image/quality,q_30`
|
||||
? parentGen.uploadResults[uploadResultIndex].url
|
||||
: null;
|
||||
|
||||
// 如果没有上传的URL,则使用asset中的URL
|
||||
const displayUrl = uploadedUrl || asset.url || asset.blobUrl;
|
||||
|
||||
// 如果URL是blob:开头但已失效,尝试重新创建
|
||||
|
||||
@@ -2,25 +2,19 @@ import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { Stage, Layer, Image as KonvaImage, Line } from 'react-konva';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { Button } from './ui/Button';
|
||||
import { ZoomIn, ZoomOut, RotateCcw, Download, Eye, EyeOff, Eraser } from 'lucide-react';
|
||||
import { cn } from '../utils/cn';
|
||||
import { ZoomIn, ZoomOut, RotateCcw, Download } from 'lucide-react';
|
||||
|
||||
export const ImageCanvas: React.FC = () => {
|
||||
const {
|
||||
canvasImage,
|
||||
canvasZoom,
|
||||
setCanvasZoom,
|
||||
canvasPan,
|
||||
setCanvasPan,
|
||||
brushStrokes,
|
||||
addBrushStroke,
|
||||
clearBrushStrokes,
|
||||
showMasks,
|
||||
setShowMasks,
|
||||
selectedTool,
|
||||
isGenerating,
|
||||
brushSize,
|
||||
setBrushSize,
|
||||
showHistory,
|
||||
showPromptPanel
|
||||
} = useAppStore();
|
||||
@@ -50,7 +44,7 @@ export const ImageCanvas: React.FC = () => {
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
}, [setCanvasZoom]);
|
||||
|
||||
// 加载图像
|
||||
useEffect(() => {
|
||||
@@ -60,7 +54,7 @@ export const ImageCanvas: React.FC = () => {
|
||||
console.log('开始加载图像,URL:', canvasImage);
|
||||
|
||||
img = new window.Image();
|
||||
let isCancelled = false;
|
||||
const isCancelled = false;
|
||||
|
||||
img.onload = () => {
|
||||
// 检查是否已取消
|
||||
@@ -195,7 +189,7 @@ export const ImageCanvas: React.FC = () => {
|
||||
image.src = '';
|
||||
}
|
||||
};
|
||||
}, [canvasImage]); // 只依赖canvasImage,避免其他依赖引起循环
|
||||
}, [canvasImage, image, setCanvasZoom, stageSize.height, stageSize.width]); // 添加所有依赖项
|
||||
|
||||
// 处理舞台大小调整
|
||||
useEffect(() => {
|
||||
@@ -212,7 +206,7 @@ export const ImageCanvas: React.FC = () => {
|
||||
updateSize();
|
||||
window.addEventListener('resize', updateSize);
|
||||
return () => window.removeEventListener('resize', updateSize);
|
||||
}, []);
|
||||
}, [showPromptPanel, showHistory]);
|
||||
|
||||
// 监听面板状态变化以调整画布大小
|
||||
useEffect(() => {
|
||||
@@ -243,14 +237,13 @@ export const ImageCanvas: React.FC = () => {
|
||||
|
||||
container.addEventListener('wheel', handleWheel, { passive: false });
|
||||
return () => container.removeEventListener('wheel', handleWheel);
|
||||
}, [canvasZoom]);
|
||||
}, [canvasZoom, handleZoom]);
|
||||
|
||||
const handleMouseDown = (e: any) => {
|
||||
const handleMouseDown = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
if (selectedTool !== 'mask' || !image) return;
|
||||
|
||||
setIsDrawing(true);
|
||||
const stage = e.target.getStage();
|
||||
const pos = stage.getPointerPosition();
|
||||
|
||||
// 使用 Konva 的 getRelativePointerPosition 获取准确坐标
|
||||
const relativePos = stage.getRelativePointerPosition();
|
||||
@@ -269,11 +262,10 @@ export const ImageCanvas: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: any) => {
|
||||
const handleMouseMove = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||
if (!isDrawing || selectedTool !== 'mask' || !image) return;
|
||||
|
||||
const stage = e.target.getStage();
|
||||
const pos = stage.getPointerPosition();
|
||||
|
||||
// 使用 Konva 的 getRelativePointerPosition 获取准确坐标
|
||||
const relativePos = stage.getRelativePointerPosition();
|
||||
@@ -351,17 +343,17 @@ export const ImageCanvas: React.FC = () => {
|
||||
// 下载第一个上传结果(通常是生成的图像)
|
||||
const uploadResult = selectedRecord.uploadResults[0];
|
||||
if (uploadResult.success && uploadResult.url) {
|
||||
// 使用async IIFE处理异步操作
|
||||
(async () => {
|
||||
try {
|
||||
// 首先尝试使用fetch获取图像数据
|
||||
const response = await fetch(uploadResult.url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
|
||||
// 创建下载链接
|
||||
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
|
||||
// 添加更多缓存控制头以绕过CDN缓存
|
||||
fetch(uploadResult.url, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
})
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -369,64 +361,21 @@ export const ImageCanvas: React.FC = () => {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
console.log('上传后的图像下载成功:', uploadResult.url);
|
||||
} catch (error) {
|
||||
console.error('使用fetch下载上传后的图像时出错:', error);
|
||||
// 如果fetch失败(可能是跨域问题),使用Canvas方案
|
||||
try {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous'; // 设置跨域属性
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 创建canvas并绘制图像
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 将canvas转换为blob并下载
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
console.log('使用Canvas方案下载成功');
|
||||
} else {
|
||||
console.error('Canvas转换为blob失败');
|
||||
}
|
||||
}, 'image/png');
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas处理失败:', canvasError);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = (imgError) => {
|
||||
console.error('图像加载失败:', imgError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
};
|
||||
|
||||
img.src = uploadResult.url;
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas方案也失败了:', canvasError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
}
|
||||
}
|
||||
})();
|
||||
URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('下载图像失败:', error);
|
||||
// 如果fetch失败,回退到直接使用a标签
|
||||
const link = document.createElement('a');
|
||||
link.href = uploadResult.url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
|
||||
// 立即返回,让异步操作在后台进行
|
||||
// 立即返回
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -463,14 +412,17 @@ export const ImageCanvas: React.FC = () => {
|
||||
document.body.removeChild(link);
|
||||
} else if (canvasImage.startsWith('blob:')) {
|
||||
// Blob URL格式
|
||||
// 使用async IIFE处理异步操作
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(canvasImage);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
|
||||
// 添加更多缓存控制头以绕过CDN缓存
|
||||
fetch(canvasImage, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
})
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -478,68 +430,32 @@ export const ImageCanvas: React.FC = () => {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
} catch (error) {
|
||||
console.error('下载Blob图像时出错:', error);
|
||||
// 如果fetch失败(可能是跨域问题),使用Canvas方案
|
||||
try {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 创建canvas并绘制图像
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 将canvas转换为blob并下载
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
console.log('使用Canvas方案下载成功');
|
||||
} else {
|
||||
console.error('Canvas转换为blob失败');
|
||||
}
|
||||
}, 'image/png');
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas处理失败:', canvasError);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = (imgError) => {
|
||||
console.error('图像加载失败:', imgError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
};
|
||||
|
||||
img.src = canvasImage;
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas方案也失败了:', canvasError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
}
|
||||
}
|
||||
})();
|
||||
URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('下载图像失败:', error);
|
||||
// 如果fetch失败,回退到直接使用a标签
|
||||
const link = document.createElement('a');
|
||||
link.href = canvasImage;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
} else {
|
||||
// 普通URL格式
|
||||
// 使用async IIFE处理异步操作
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(canvasImage);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
// 使用fetch获取图像数据并创建Blob URL以确保正确下载
|
||||
// 添加更多缓存控制头以绕过CDN缓存
|
||||
fetch(canvasImage, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
})
|
||||
.then(response => response.blob())
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -547,59 +463,19 @@ export const ImageCanvas: React.FC = () => {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
} catch (error) {
|
||||
console.error('下载图像时出错:', error);
|
||||
// 如果fetch失败(可能是跨域问题),使用Canvas方案
|
||||
try {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous'; // 设置跨域属性
|
||||
img.onload = () => {
|
||||
try {
|
||||
// 创建canvas并绘制图像
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 将canvas转换为blob并下载
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理创建的URL
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
|
||||
console.log('使用Canvas方案下载成功');
|
||||
} else {
|
||||
console.error('Canvas转换为blob失败');
|
||||
}
|
||||
}, 'image/png');
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas处理失败:', canvasError);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = (imgError) => {
|
||||
console.error('图像加载失败:', imgError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
};
|
||||
|
||||
img.src = canvasImage;
|
||||
} catch (canvasError) {
|
||||
console.error('Canvas方案也失败了:', canvasError);
|
||||
console.log('下载失败,未执行回退方案');
|
||||
}
|
||||
}
|
||||
})();
|
||||
URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('下载图像失败:', error);
|
||||
// 如果fetch失败,回退到直接使用a标签
|
||||
const link = document.createElement('a');
|
||||
link.href = canvasImage;
|
||||
link.download = `nano-banana-${Date.now()}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -647,7 +523,7 @@ export const ImageCanvas: React.FC = () => {
|
||||
width={stageSize.width}
|
||||
height={stageSize.height}
|
||||
draggable={selectedTool !== 'mask'}
|
||||
onDragEnd={(e) => {
|
||||
onDragEnd={() => {
|
||||
// 通过stageRef直接获取和设置位置
|
||||
const stage = stageRef.current;
|
||||
if (stage) {
|
||||
|
||||
@@ -1,13 +1,101 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Textarea } from './ui/Textarea';
|
||||
import { Button } from './ui/Button';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { useImageGeneration, useImageEditing } from '../hooks/useImageGeneration';
|
||||
import { Upload, Wand2, Edit3, MousePointer, HelpCircle, ChevronDown, ChevronRight, RotateCcw } from 'lucide-react';
|
||||
import { blobToBase64, urlToBlob } from '../utils/imageUtils';
|
||||
import { urlToBlob } from '../utils/imageUtils';
|
||||
import { PromptHints } from './PromptHints';
|
||||
import { PromptSuggestions } from './PromptSuggestions';
|
||||
import { cn } from '../utils/cn';
|
||||
import * as referenceImageService from '../services/referenceImageService';
|
||||
|
||||
// 图像预览组件
|
||||
const ImagePreview: React.FC<{
|
||||
image: string;
|
||||
index: number;
|
||||
selectedTool: 'generate' | 'edit' | 'mask';
|
||||
onRemove: () => void;
|
||||
}> = ({ image, index, onRemove }) => {
|
||||
const [imageSrc, setImageSrc] = useState<string>(image);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 如果是IndexedDB图像,需要获取实际的Blob数据
|
||||
if (image.startsWith('indexeddb://')) {
|
||||
const imageId = image.replace('indexeddb://', '');
|
||||
referenceImageService.getReferenceImage(imageId)
|
||||
.then(blob => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
setImageSrc(url);
|
||||
setLoading(false);
|
||||
} else {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('获取参考图像失败:', err);
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
// 对于其他类型的URL,直接使用
|
||||
setImageSrc(image);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [image]);
|
||||
|
||||
// 清理创建的Blob URL
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (imageSrc.startsWith('blob:') && imageSrc !== image) {
|
||||
URL.revokeObjectURL(imageSrc);
|
||||
}
|
||||
};
|
||||
}, [imageSrc, image]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="relative w-full h-20 rounded-lg border-2 border-gray-200 bg-gray-100 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="relative w-full h-20 rounded-lg border-2 border-gray-200 bg-red-50 flex items-center justify-center">
|
||||
<span className="text-red-500 text-sm">图像加载失败</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={`参考图像 ${index + 1}`}
|
||||
className="w-full h-20 object-cover rounded-lg border-2 border-gray-200"
|
||||
onError={() => setError(true)}
|
||||
/>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1.5 transition-colors shadow-md hover:bg-red-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-2 py-1 rounded-lg">
|
||||
参考 {index + 1}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const PromptComposer: React.FC = () => {
|
||||
const {
|
||||
@@ -32,8 +120,7 @@ export const PromptComposer: React.FC = () => {
|
||||
setCanvasImage,
|
||||
showPromptPanel,
|
||||
setShowPromptPanel,
|
||||
clearBrushStrokes,
|
||||
addBlob
|
||||
clearBrushStrokes
|
||||
} = useAppStore();
|
||||
|
||||
const { generate, cancelGeneration } = useImageGeneration();
|
||||
@@ -45,12 +132,27 @@ export const PromptComposer: React.FC = () => {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 初始化参考图像数据库
|
||||
useEffect(() => {
|
||||
const initDB = async () => {
|
||||
try {
|
||||
await referenceImageService.initReferenceImageDB();
|
||||
console.log('参考图像数据库初始化成功');
|
||||
} catch (error) {
|
||||
console.error('参考图像数据库初始化失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initDB();
|
||||
}, []);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!currentPrompt.trim()) return;
|
||||
|
||||
if (selectedTool === 'generate') {
|
||||
// 将上传的图像转换为Blob对象
|
||||
const referenceImageBlobs: Blob[] = [];
|
||||
|
||||
for (const img of uploadedImages) {
|
||||
if (img.startsWith('data:')) {
|
||||
// 从base64数据创建Blob
|
||||
@@ -63,6 +165,23 @@ export const PromptComposer: React.FC = () => {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
referenceImageBlobs.push(new Blob([ab], { type: mimeString }));
|
||||
} else if (img.startsWith('indexeddb://')) {
|
||||
// 从IndexedDB获取参考图像
|
||||
const imageId = img.replace('indexeddb://', '');
|
||||
try {
|
||||
const blob = await referenceImageService.getReferenceImage(imageId);
|
||||
if (blob) {
|
||||
referenceImageBlobs.push(blob);
|
||||
} else {
|
||||
console.warn('无法从IndexedDB获取参考图像:', imageId);
|
||||
// 如果无法获取图像,尝试重新上传
|
||||
console.log('尝试重新处理参考图像...');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法从IndexedDB获取参考图像:', imageId, error);
|
||||
// 如果无法获取图像,尝试重新上传
|
||||
console.log('尝试重新处理参考图像...');
|
||||
}
|
||||
} else if (img.startsWith('blob:')) {
|
||||
// 从Blob URL获取Blob
|
||||
const { getBlob } = useAppStore.getState();
|
||||
@@ -75,17 +194,9 @@ export const PromptComposer: React.FC = () => {
|
||||
const response = await fetch(img);
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
// 重新添加到AppStore
|
||||
const newUrl = useAppStore.getState().addBlob(blob);
|
||||
referenceImageBlobs.push(blob);
|
||||
// 更新uploadedImages中的URL
|
||||
const index = uploadedImages.indexOf(img);
|
||||
if (index !== -1) {
|
||||
const newImages = [...uploadedImages];
|
||||
newImages[index] = newUrl;
|
||||
useAppStore.getState().clearUploadedImages();
|
||||
newImages.forEach(imageUrl => useAppStore.getState().addUploadedImage(imageUrl));
|
||||
}
|
||||
} else {
|
||||
console.warn('无法重新获取参考图像:', img);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法重新获取参考图像:', img, error);
|
||||
@@ -102,9 +213,13 @@ export const PromptComposer: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤掉无效的Blob,只保留有效的参考图像
|
||||
const validBlobs = referenceImageBlobs.filter(blob => blob.size > 0);
|
||||
|
||||
// 即使没有参考图像也继续生成,因为提示文本是必需的
|
||||
generate({
|
||||
prompt: currentPrompt,
|
||||
referenceImages: referenceImageBlobs.length > 0 ? referenceImageBlobs : undefined,
|
||||
referenceImages: validBlobs.length > 0 ? validBlobs : undefined,
|
||||
temperature,
|
||||
seed: seed !== null ? seed : undefined
|
||||
});
|
||||
@@ -116,28 +231,36 @@ export const PromptComposer: React.FC = () => {
|
||||
const handleFileUpload = async (file: File) => {
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
try {
|
||||
// 直接使用Blob创建URL
|
||||
const blobUrl = addBlob(file);
|
||||
// 保存参考图像到IndexedDB
|
||||
const imageId = await referenceImageService.saveReferenceImage(file);
|
||||
|
||||
// 创建一个特殊的URL来标识这是存储在IndexedDB中的图像
|
||||
const imageUrl = `indexeddb://${imageId}`;
|
||||
|
||||
if (selectedTool === 'generate') {
|
||||
// 添加到参考图像(最多2张)
|
||||
if (uploadedImages.length < 2) {
|
||||
addUploadedImage(blobUrl);
|
||||
addUploadedImage(imageUrl);
|
||||
}
|
||||
} else if (selectedTool === 'edit') {
|
||||
// 编辑模式下,添加到单独的编辑参考图像(最多2张)
|
||||
if (editReferenceImages.length < 2) {
|
||||
addEditReferenceImage(blobUrl);
|
||||
addEditReferenceImage(imageUrl);
|
||||
}
|
||||
// 如果没有画布图像,则设置为画布图像
|
||||
if (!canvasImage) {
|
||||
setCanvasImage(blobUrl);
|
||||
setCanvasImage(imageUrl);
|
||||
}
|
||||
} else if (selectedTool === 'mask') {
|
||||
// 遮罩模式下,立即设置为画布图像
|
||||
clearUploadedImages();
|
||||
addUploadedImage(blobUrl);
|
||||
setCanvasImage(blobUrl);
|
||||
// 遮罩模式下,将图像添加为参考图像而不是清除现有图像
|
||||
// 只有在没有画布图像时才设置为画布图像
|
||||
if (!canvasImage) {
|
||||
setCanvasImage(imageUrl);
|
||||
}
|
||||
// 不清除现有的上传图像,而是将新图像添加为参考图像(如果还有空间)
|
||||
if (uploadedImages.length < 2) {
|
||||
addUploadedImage(imageUrl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传图像失败:', error);
|
||||
@@ -171,7 +294,7 @@ export const PromptComposer: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearSession = () => {
|
||||
const handleClearSession = async () => {
|
||||
setCurrentPrompt('');
|
||||
clearUploadedImages();
|
||||
clearEditReferenceImages();
|
||||
@@ -180,6 +303,14 @@ export const PromptComposer: React.FC = () => {
|
||||
setSeed(null);
|
||||
setTemperature(0.7);
|
||||
setShowClearConfirm(false);
|
||||
|
||||
// 清空IndexedDB中的所有参考图像
|
||||
try {
|
||||
await referenceImageService.clearAllReferenceImages();
|
||||
console.log('已清空IndexedDB中的所有参考图像');
|
||||
} catch (error) {
|
||||
console.error('清空IndexedDB中的参考图像失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const tools = [
|
||||
@@ -324,25 +455,13 @@ export const PromptComposer: React.FC = () => {
|
||||
(selectedTool === 'edit' && editReferenceImages.length > 0)) && (
|
||||
<div className="space-y-2.5">
|
||||
{(selectedTool === 'generate' ? uploadedImages : editReferenceImages).map((image, index) => (
|
||||
<div key={index} className="relative">
|
||||
<img
|
||||
src={image}
|
||||
alt={`参考图像 ${index + 1}`}
|
||||
className="w-full h-20 object-cover rounded-lg border-2 border-gray-200"
|
||||
/>
|
||||
<button
|
||||
onClick={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
|
||||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1.5 transition-colors shadow-md hover:bg-red-600"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-2 py-1 rounded-lg">
|
||||
参考 {index + 1}
|
||||
</div>
|
||||
</div>
|
||||
<ImagePreview
|
||||
key={index}
|
||||
image={image}
|
||||
index={index}
|
||||
selectedTool={selectedTool}
|
||||
onRemove={() => selectedTool === 'generate' ? removeUploadedImage(index) : removeEditReferenceImage(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -423,7 +542,7 @@ export const PromptComposer: React.FC = () => {
|
||||
<div className="mt-4 animate-in slide-down duration-300">
|
||||
<PromptSuggestions
|
||||
onWordSelect={(word) => {
|
||||
setCurrentPrompt(currentPrompt ? `${currentPrompt}, ${word}` : word);
|
||||
setCurrentPrompt(currentPrompt ? `${currentPrompt};${word}` : word);
|
||||
}}
|
||||
minFrequency={3}
|
||||
showTitle={false}
|
||||
@@ -493,7 +612,7 @@ export const PromptComposer: React.FC = () => {
|
||||
<div className="flex space-x-3">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleClearSession}
|
||||
onClick={async () => await handleClearSession()}
|
||||
className="flex-1 h-10 text-sm font-semibold card text-gray-700"
|
||||
>
|
||||
确认
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Button } from './ui/Button';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { cn } from '../utils/cn';
|
||||
@@ -18,17 +18,17 @@ export const PromptSuggestions: React.FC<{
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
// 从提示词中提取词语并统计频次
|
||||
const extractWords = (text: string): string[] => {
|
||||
const extractWords = useCallback((text: string): string[] => {
|
||||
// 移除标点符号并分割词语
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s\u4e00-\u9fff]/g, ' ') // 保留中文字符
|
||||
.split(/\s+/)
|
||||
.filter(word => word.length > 1); // 过滤掉单字符
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 统计词语频次
|
||||
const calculateWordFrequency = (): WordFrequency[] => {
|
||||
const calculateWordFrequency = useCallback((): WordFrequency[] => {
|
||||
const wordCount: Record<string, number> = {};
|
||||
|
||||
// 收集所有提示词
|
||||
@@ -52,7 +52,7 @@ export const PromptSuggestions: React.FC<{
|
||||
});
|
||||
}
|
||||
|
||||
// 提取词语并统计频次
|
||||
// 统计词语频次
|
||||
allPrompts.forEach(prompt => {
|
||||
const words = extractWords(prompt);
|
||||
words.forEach(word => {
|
||||
@@ -65,11 +65,11 @@ export const PromptSuggestions: React.FC<{
|
||||
.map(([word, count]) => ({ word, count }))
|
||||
.filter(({ count }) => count >= minFrequency)
|
||||
.sort((a, b) => b.count - a.count);
|
||||
};
|
||||
}, [currentProject, minFrequency, extractWords]);
|
||||
|
||||
useEffect(() => {
|
||||
setFrequentWords(calculateWordFrequency());
|
||||
}, [currentProject, minFrequency]);
|
||||
}, [currentProject, minFrequency, calculateWordFrequency]);
|
||||
|
||||
// 显示的词语数量
|
||||
const displayWords = showAll ? frequentWords : frequentWords.slice(0, 20);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useReducer, useState, useEffect, useRef } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useReducer, useRef } from 'react';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
import React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement>,
|
||||
VariantProps<typeof inputVariants> {
|
||||
// Additional props can be added here if needed
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
const inputVariants = cva(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border-gray-300 focus-visible:ring-yellow-400',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, variant, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
className={cn(inputVariants({ variant, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
Input.displayName = 'Input';
|
||||
export { Input };
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../utils/cn';
|
||||
|
||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
VariantProps<typeof textareaVariants> {
|
||||
// Additional props can be added here if needed
|
||||
}
|
||||
|
||||
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
|
||||
Reference in New Issue
Block a user