diff --git a/src/App.tsx b/src/App.tsx index 4b1064d..2ed90a3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,7 @@ const queryClient = new QueryClient({ function AppContent() { useKeyboardShortcuts(); - const { showPromptPanel, setShowPromptPanel, showHistory, setShowHistory } = useAppStore(); + const { showPromptPanel, setShowPromptPanel, setShowHistory } = useAppStore(); // 在挂载时初始化IndexedDB useEffect(() => { @@ -59,7 +59,7 @@ function AppContent() {
-
+
diff --git a/src/components/HistoryPanel.tsx b/src/components/HistoryPanel.tsx index 0ff77a6..968600c 100644 --- a/src/components/HistoryPanel.tsx +++ b/src/components/HistoryPanel.tsx @@ -47,7 +47,7 @@ export const HistoryPanel: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); // 悬浮预览状态 - const [hoveredImage, setHoveredImage] = useState<{url: string, title: string} | null>(null); + const [hoveredImage, setHoveredImage] = useState<{url: string, title: string, width?: number, height?: number, size?: number} | null>(null); const [previewPosition, setPreviewPosition] = useState<{x: number, y: number}>({x: 0, y: 0}); const generations = currentProject?.generations || []; @@ -211,7 +211,7 @@ export const HistoryPanel: React.FC = () => { } return ( -
+
{/* 头部 */}
@@ -298,7 +298,7 @@ export const HistoryPanel: React.FC = () => {

暂无历史记录

) : ( -
+
{/* 显示生成记录 */} {[...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((generation, index) => (
{ } }} onMouseEnter={(e) => { - if (generation.outputAssets && generation.outputAssets.length > 0) { - const asset = generation.outputAssets[0]; - if (asset.url) { + // 优先使用上传后的远程链接,如果没有则使用原始链接 + let imageUrl = getUploadedImageUrl(generation, 0); + if (!imageUrl && generation.outputAssets && generation.outputAssets.length > 0) { + imageUrl = generation.outputAssets[0].url; + } + if (imageUrl) { + // 创建图像对象以获取尺寸 + const img = new Image(); + img.onload = () => { + // 计算文件大小(仅对base64数据) + let size = 0; + if (imageUrl.startsWith('data:')) { + // 估算base64数据大小 + const base64Data = imageUrl.split(',')[1]; + size = Math.round((base64Data.length * 3) / 4); + } + setHoveredImage({ - url: asset.url, + url: imageUrl, title: `生成记录 G${index + 1}`, - description: generation.prompt + width: img.width, + height: img.height, + size: size }); - setPreviewPosition({x: e.clientX, y: e.clientY}); - } + + // 计算预览位置,确保不超出屏幕边界 + const previewWidth = 300; + const previewHeight = 300; + const offsetX = 10; + const offsetY = 10; + + + // 获取HistoryPanel的位置 + const historyPanel = document.querySelector('.w-72.bg-white.p-4'); + const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 }; + + // 计算相对于HistoryPanel的位置 + let x = e.clientX - panelRect.left + offsetX; + let y = e.clientY - panelRect.top + offsetY; + + + + // 确保预览窗口不会超出右边界 + if (x + previewWidth > window.innerWidth) { + x = window.innerWidth - previewWidth - 10; + } + + // 确保预览窗口不会超出下边界 + if (y + previewHeight > window.innerHeight) { + y = window.innerHeight - previewHeight - 10; + } + + // 确保预览窗口不会超出左边界 + if (x < 0) { + x = 10; + } + + // 确保预览窗口不会超出上边界 + if (y < 0) { + y = 10; + } + + // 添加额外的安全边界检查 + x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); + y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); + + setPreviewPosition({x, y}); + }; + img.onerror = (error) => { + console.error('图像加载失败:', error); + // 即使图像加载失败,也显示预览 + setHoveredImage({ + url: imageUrl, + title: `生成记录 G${index + 1}`, + width: 0, + height: 0, + size: 0 + }); + + // 计算预览位置 + const previewWidth = 300; + const previewHeight = 300; + const offsetX = 10; + const offsetY = 10; + + + let x = e.clientX + offsetX; + let y = e.clientY + offsetY; + + // 确保预览窗口不会超出右边界 + if (x + previewWidth > window.innerWidth) { + x = window.innerWidth - previewWidth - 10; + } + + // 确保预览窗口不会超出下边界 + if (y + previewHeight > window.innerHeight) { + y = window.innerHeight - previewHeight - 10; + } + + // 确保预览窗口不会超出左边界 + if (x < 0) { + x = 10; + } + + // 确保预览窗口不会超出上边界 + if (y < 0) { + y = 10; + } + + // 添加额外的安全边界检查 + x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); + y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); + + setPreviewPosition({x, y}); + }; + img.src = imageUrl; } }} onMouseMove={(e) => { @@ -339,58 +445,62 @@ export const HistoryPanel: React.FC = () => { const offsetX = 10; const offsetY = 10; - let x = e.clientX + offsetX; - let y = e.clientY + offsetY; - // 检查是否超出右边界 - if (x + previewWidth > window.innerWidth) { - x = window.innerWidth - previewWidth - 10; + // 获取HistoryPanel的位置 + const historyPanel = document.querySelector('.w-72.bg-white.p-4'); + const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 }; + + // 计算相对于HistoryPanel的位置 + let x = e.clientX - panelRect.left + offsetX; + let y = e.clientY - panelRect.top + offsetY; + + // 确保预览窗口不会超出右边界 + if (x + previewWidth > (historyPanel ? historyPanel.clientWidth : window.innerWidth)) { + x = (historyPanel ? historyPanel.clientWidth : window.innerWidth) - previewWidth - 10; } - // 检查是否超出下边界 - if (y + previewHeight > window.innerHeight) { - y = window.innerHeight - previewHeight - 10; + // 确保预览窗口不会超出下边界 + if (y + previewHeight > (historyPanel ? historyPanel.clientHeight : window.innerHeight)) { + y = (historyPanel ? historyPanel.clientHeight : window.innerHeight) - previewHeight - 10; } - // 检查是否超出左边界 + // 确保预览窗口不会超出左边界 if (x < 0) { x = 10; } - // 检查是否超出上边界 + // 确保预览窗口不会超出上边界 if (y < 0) { y = 10; } + // 添加额外的安全边界检查 + const maxWidth = historyPanel ? historyPanel.clientWidth : window.innerWidth; + const maxHeight = historyPanel ? historyPanel.clientHeight : window.innerHeight; + x = Math.max(10, Math.min(x, maxWidth - previewWidth - 10)); + y = Math.max(10, Math.min(y, maxHeight - previewHeight - 10)); + setPreviewPosition({x, y}); }} onMouseLeave={() => { setHoveredImage(null); }} > - {generation.outputAssets && generation.outputAssets.length > 0 ? ( - (() => { - const asset = generation.outputAssets[0]; - if (asset.url) { - // 如果是base64数据URL,直接显示 - if (asset.url.startsWith('data:')) { - return 生成的变体; - } - // 如果是普通URL,直接显示 - return 生成的变体; - } else { - return ( -
- -
- ); - } - })() - ) : ( -
- -
- )} + {(() => { + // 优先使用上传后的远程链接,如果没有则使用原始链接 + const imageUrl = getUploadedImageUrl(generation, 0) || + (generation.outputAssets && generation.outputAssets.length > 0 ? generation.outputAssets[0].url : null); + + if (imageUrl) { + return 生成的变体; + } else { + return ( +
+ +
+ ); + } + })()} {/* 变体编号 */}
@@ -421,16 +531,120 @@ export const HistoryPanel: React.FC = () => { } }} onMouseEnter={(e) => { - if (edit.outputAssets && edit.outputAssets.length > 0) { - const asset = edit.outputAssets[0]; - if (asset.url) { + // 优先使用上传后的远程链接,如果没有则使用原始链接 + let imageUrl = getUploadedImageUrl(edit, 0); + if (!imageUrl && edit.outputAssets && edit.outputAssets.length > 0) { + imageUrl = edit.outputAssets[0].url; + } + + if (imageUrl) { + // 创建图像对象以获取尺寸 + const img = new Image(); + img.onload = () => { + // 计算文件大小(仅对base64数据) + let size = 0; + if (imageUrl.startsWith('data:')) { + // 估算base64数据大小 + const base64Data = imageUrl.split(',')[1]; + size = Math.round((base64Data.length * 3) / 4); + } + setHoveredImage({ - url: asset.url, + url: imageUrl, title: `编辑记录 E${index + 1}`, - description: edit.instruction + width: img.width, + height: img.height, + size: size }); - setPreviewPosition({x: e.clientX, y: e.clientY}); - } + + // 计算预览位置,确保不超出屏幕边界 + const previewWidth = 300; + const previewHeight = 300; + const offsetX = 10; + const offsetY = 10; + + + let x = e.clientX + offsetX; + let y = e.clientY + offsetY; + + // 确保预览窗口不会超出右边界 + if (x + previewWidth > window.innerWidth) { + x = window.innerWidth - previewWidth - 10; + } + + // 确保预览窗口不会超出下边界 + if (y + previewHeight > window.innerHeight) { + y = window.innerHeight - previewHeight - 10; + } + + // 确保预览窗口不会超出左边界 + if (x < 0) { + x = 10; + } + + // 确保预览窗口不会超出上边界 + if (y < 0) { + y = 10; + } + + // 添加额外的安全边界检查 + x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); + y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); + + setPreviewPosition({x, y}); + }; + img.onerror = (error) => { + console.error('图像加载失败:', error); + // 即使图像加载失败,也显示预览 + setHoveredImage({ + url: imageUrl, + title: `编辑记录 E${index + 1}`, + width: 0, + height: 0, + size: 0 + }); + + // 计算预览位置 + const previewWidth = 300; + const previewHeight = 300; + const offsetX = 10; + const offsetY = 10; + + // 获取HistoryPanel的位置信息 + const historyPanel = e.currentTarget.closest('.w-72'); + const panelRect = historyPanel ? historyPanel.getBoundingClientRect() : { left: 0, top: 0 }; + + // 计算相对于整个视窗的位置 + let x = e.clientX + offsetX; + let y = e.clientY + offsetY; + + // 确保预览窗口不会超出右边界 + if (x + previewWidth > window.innerWidth) { + x = window.innerWidth - previewWidth - 10; + } + + // 确保预览窗口不会超出下边界 + if (y + previewHeight > window.innerHeight) { + y = window.innerHeight - previewHeight - 10; + } + + // 确保预览窗口不会超出左边界 + if (x < 0) { + x = 10; + } + + // 确保预览窗口不会超出上边界 + if (y < 0) { + y = 10; + } + + // 添加额外的安全边界检查 + x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); + y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); + + setPreviewPosition({x, y}); + }; + img.src = imageUrl; } }} onMouseMove={(e) => { @@ -443,55 +657,51 @@ export const HistoryPanel: React.FC = () => { let x = e.clientX + offsetX; let y = e.clientY + offsetY; - // 检查是否超出右边界 + // 确保预览窗口不会超出右边界 if (x + previewWidth > window.innerWidth) { x = window.innerWidth - previewWidth - 10; } - // 检查是否超出下边界 + // 确保预览窗口不会超出下边界 if (y + previewHeight > window.innerHeight) { y = window.innerHeight - previewHeight - 10; } - // 检查是否超出左边界 + // 确保预览窗口不会超出左边界 if (x < 0) { x = 10; } - // 检查是否超出上边界 + // 确保预览窗口不会超出上边界 if (y < 0) { y = 10; } + // 添加额外的安全边界检查 + x = Math.max(10, Math.min(x, window.innerWidth - previewWidth - 10)); + y = Math.max(10, Math.min(y, window.innerHeight - previewHeight - 10)); + setPreviewPosition({x, y}); }} onMouseLeave={() => { setHoveredImage(null); }} > - {edit.outputAssets && edit.outputAssets.length > 0 ? ( - (() => { - const asset = edit.outputAssets[0]; - if (asset.url) { - // 如果是base64数据URL,直接显示 - if (asset.url.startsWith('data:')) { - return 编辑的变体; - } - // 如果是普通URL,直接显示 - return 编辑的变体; - } else { - return ( -
- -
- ); - } - })() - ) : ( -
- -
- )} + {(() => { + // 优先使用上传后的远程链接,如果没有则使用原始链接 + const imageUrl = getUploadedImageUrl(edit, 0) || + (edit.outputAssets && edit.outputAssets.length > 0 ? edit.outputAssets[0].url : null); + + if (imageUrl) { + return 编辑的变体; + } else { + return ( +
+ +
+ ); + } + })()} {/* 编辑标签 */}
@@ -573,27 +783,36 @@ export const HistoryPanel: React.FC = () => { {gen.sourceAssets.length} 个参考图像
- {gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => ( -
{ - e.stopPropagation(); - setPreviewModal({ - open: true, - imageUrl: asset.url, - title: `参考图像 ${index + 1}`, - description: `${asset.width} × ${asset.height}` - }); - }} - > - {`参考图像 -
- ))} + {gen.sourceAssets.slice(0, 4).map((asset: any, index: number) => { + // 获取上传后的远程链接(如果存在) + // 参考图像在uploadResults中从索引1开始(索引0是生成的图像) + const uploadedUrl = gen.uploadResults && gen.uploadResults[index + 1] && gen.uploadResults[index + 1].success + ? `${gen.uploadResults[index + 1].url}?x-oss-process=image/quality,q_50` + : null; + const displayUrl = uploadedUrl || asset.url; + + return ( +
{ + e.stopPropagation(); + setPreviewModal({ + open: true, + imageUrl: displayUrl, + title: `参考图像 ${index + 1}`, + description: `${asset.width} × ${asset.height}` + }); + }} + > + {`参考图像 +
+ ); + })} {gen.sourceAssets.length > 4 && (
+{gen.sourceAssets.length - 4} @@ -672,27 +891,36 @@ export const HistoryPanel: React.FC = () => { 原始参考图像:
- {parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => ( -
{ - e.stopPropagation(); - setPreviewModal({ - open: true, - imageUrl: asset.url, - title: `原始参考图像 ${index + 1}`, - description: `${asset.width} × ${asset.height}` - }); - }} - > - {`原始参考图像 -
- ))} + {parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => { + // 获取上传后的远程链接(如果存在) + // 参考图像在uploadResults中从索引1开始(索引0是生成的图像) + const uploadedUrl = parentGen.uploadResults && parentGen.uploadResults[index + 1] && parentGen.uploadResults[index + 1].success + ? `${parentGen.uploadResults[index + 1].url}?x-oss-process=image/quality,q_50` + : null; + const displayUrl = uploadedUrl || asset.url; + + return ( +
{ + e.stopPropagation(); + setPreviewModal({ + open: true, + imageUrl: displayUrl, + title: `原始参考图像 ${index + 1}`, + description: `${asset.width} × ${asset.height}` + }); + }} + > + {`原始参考图像 +
+ ); + })} {parentGen.sourceAssets.length > 4 && (
+{parentGen.sourceAssets.length - 4} @@ -715,6 +943,28 @@ export const HistoryPanel: React.FC = () => { })()}
+ {/* 测试按钮 - 用于调试 */} +
+ +
+ {/* 操作 */}
-
+
{tools.map((tool) => ( ))} @@ -203,29 +203,29 @@ export const PromptComposer: React.FC = () => {
{/* 文件上传 */} -
+
-