优化界面

This commit is contained in:
yuantao
2025-09-16 18:38:02 +08:00
parent e0600f5d50
commit 2345ed80f1
15 changed files with 725 additions and 266 deletions

View File

@@ -47,7 +47,7 @@ export const HistoryPanel: React.FC = () => {
const [searchTerm, setSearchTerm] = useState<string>('');
// 悬浮预览状态
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 (
<div className="w-72 bg-white p-4 flex flex-col h-full">
<div className="w-72 bg-white p-4 flex flex-col h-full relative">
{/* 头部 */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-2">
@@ -298,7 +298,7 @@ export const HistoryPanel: React.FC = () => {
<p className="text-xs text-gray-400"></p>
</div>
) : (
<div className="grid grid-cols-3 gap-1.5 max-h-72 overflow-y-auto">
<div className="grid grid-cols-3 gap-1.5 max-h-72 relative">
{/* 显示生成记录 */}
{[...filteredGenerations].sort((a, b) => b.timestamp - a.timestamp).slice(0, 50).map((generation, index) => (
<div
@@ -320,16 +320,122 @@ export const HistoryPanel: React.FC = () => {
}
}}
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 <img src={asset.url} alt="生成的变体" className="w-full h-full object-cover" />;
}
// 如果是普通URL直接显示
return <img src={asset.url} alt="生成的变体" className="w-full h-full object-cover" />;
} else {
return (
<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 className="w-full h-full bg-gray-100 flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-gray-400" />
</div>
)}
{(() => {
// 优先使用上传后的远程链接,如果没有则使用原始链接
const imageUrl = getUploadedImageUrl(generation, 0) ||
(generation.outputAssets && generation.outputAssets.length > 0 ? generation.outputAssets[0].url : null);
if (imageUrl) {
return <img src={imageUrl} alt="生成的变体" className="w-full h-full object-cover" />;
} else {
return (
<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 className="absolute top-1 left-1 bg-gray-900/80 text-xs px-1 py-0.5 rounded text-white">
@@ -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 <img src={asset.url} alt="编辑的变体" className="w-full h-full object-cover" />;
}
// 如果是普通URL直接显示
return <img src={asset.url} alt="编辑的变体" className="w-full h-full object-cover" />;
} else {
return (
<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 className="w-full h-full bg-gray-100 flex items-center justify-center">
<ImageIcon className="h-4 w-4 text-gray-400" />
</div>
)}
{(() => {
// 优先使用上传后的远程链接,如果没有则使用原始链接
const imageUrl = getUploadedImageUrl(edit, 0) ||
(edit.outputAssets && edit.outputAssets.length > 0 ? edit.outputAssets[0].url : null);
if (imageUrl) {
return <img src={imageUrl} alt="编辑的变体" className="w-full h-full object-cover" />;
} else {
return (
<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 className="absolute top-1 left-1 bg-purple-900/80 text-xs px-1 py-0.5 rounded text-white">
@@ -573,27 +783,36 @@ 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) => (
<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: asset.url,
title: `参考图像 ${index + 1}`,
description: `${asset.width} × ${asset.height}`
});
}}
>
<img
src={asset.url}
alt={`参考图像 ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
{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 (
<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}`
});
}}
>
<img
src={displayUrl}
alt={`参考图像 ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
);
})}
{gen.sourceAssets.length > 4 && (
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
+{gen.sourceAssets.length - 4}
@@ -672,27 +891,36 @@ export const HistoryPanel: React.FC = () => {
:
</div>
<div className="flex flex-wrap gap-2">
{parentGen.sourceAssets.slice(0, 4).map((asset: any, index: number) => (
<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: asset.url,
title: `原始参考图像 ${index + 1}`,
description: `${asset.width} × ${asset.height}`
});
}}
>
<img
src={asset.url}
alt={`原始参考图像 ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
{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 (
<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}`
});
}}
>
<img
src={displayUrl}
alt={`原始参考图像 ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
);
})}
{parentGen.sourceAssets.length > 4 && (
<div className="w-16 h-16 rounded border flex items-center justify-center bg-gray-100 text-xs text-gray-500">
+{parentGen.sourceAssets.length - 4}
@@ -715,6 +943,28 @@ export const HistoryPanel: React.FC = () => {
})()}
</div>
{/* 测试按钮 - 用于调试 */}
<div className="mb-2">
<Button
variant="outline"
size="sm"
className="w-full h-8 text-xs card"
onClick={() => {
// 测试悬浮预览功能
setHoveredImage({
url: 'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=200',
title: '测试图像',
width: 200,
height: 200,
size: 102400
});
setPreviewPosition({x: 100, y: 100});
}}
>
</Button>
</div>
{/* 操作 */}
<div className="space-y-2 flex-shrink-0 pt-2 border-t border-gray-100">
<Button
@@ -779,12 +1029,12 @@ export const HistoryPanel: React.FC = () => {
{/* 悬浮预览 */}
{hoveredImage && (
<div
className="fixed z-50 shadow-2xl border border-gray-300 rounded-lg overflow-hidden bg-white backdrop-blur-sm"
className="absolute z-[9999] shadow-2xl border border-gray-300 rounded-lg overflow-hidden bg-white backdrop-blur-sm pointer-events-none"
style={{
left: Math.min(previewPosition.x + 10, window.innerWidth - 250),
top: Math.min(previewPosition.y + 10, window.innerHeight - 250),
maxWidth: '250px',
maxHeight: '250px'
left: `${previewPosition.x}px`,
top: `${previewPosition.y}px`,
maxWidth: '300px',
maxHeight: '300px'
}}
>
<div className="bg-gray-900 text-white text-xs p-2 truncate font-medium">
@@ -793,20 +1043,22 @@ export const HistoryPanel: React.FC = () => {
<img
src={hoveredImage.url}
alt="预览"
className="w-full h-auto max-h-[150px] object-contain"
className="w-full h-auto max-h-[200px] object-contain"
/>
{/* 图像信息 */}
<div className="p-2 bg-gray-50 border-t border-gray-200 text-xs">
{imageDimensions && (
{hoveredImage.width && hoveredImage.height && (
<div className="flex justify-between text-gray-600">
<span>:</span>
<span className="text-gray-800">{imageDimensions.width} × {imageDimensions.height}</span>
<span className="text-gray-800">{hoveredImage.width} × {hoveredImage.height}</span>
</div>
)}
{hoveredImage.size && (
<div className="flex justify-between text-gray-600 mt-1">
<span>:</span>
<span className="text-gray-800">{Math.round(hoveredImage.size / 1024)} KB</span>
</div>
)}
<div className="flex justify-between text-gray-600 mt-1">
<span>:</span>
<span className="text-gray-800 capitalize">{selectedTool}</span>
</div>
</div>
</div>
)}