You've already forked Nano-Banana-AI-Image-Editor
优化 整体UI调整
This commit is contained in:
14
src/App.tsx
14
src/App.tsx
@@ -53,18 +53,24 @@ function AppContent() {
|
|||||||
}, [setShowPromptPanel, setShowHistory]);
|
}, [setShowPromptPanel, setShowHistory]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-white text-gray-900 flex flex-col font-sans">
|
<div className="h-screen bg-gray-50 text-gray-900 flex flex-col font-sans">
|
||||||
<Header />
|
<div className="shadow-sm">
|
||||||
|
<Header />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
<div className={cn("flex-shrink-0 transition-all duration-300", !showPromptPanel && "w-8")}>
|
<div className={cn("flex-shrink-0 transition-all duration-300", !showPromptPanel && "w-8")}>
|
||||||
<PromptComposer />
|
<div className="h-full shadow-lg">
|
||||||
|
<PromptComposer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<ImageCanvas />
|
<ImageCanvas />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<HistoryPanel />
|
<div className="h-full shadow-lg">
|
||||||
|
<HistoryPanel />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const Header: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="h-12 bg-white border-b border-gray-100 flex items-center justify-between px-3">
|
<header className="h-12 bg-white border-b border-gray-100 flex items-center justify-between px-3 shadow-sm">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-yellow-50">
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-yellow-50">
|
||||||
<div className="text-lg">🍌</div>
|
<div className="text-lg">🍌</div>
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-72 bg-white border-l border-gray-200 p-4 flex flex-col h-full">
|
<div className="w-72 bg-white border-l border-gray-200 p-4 flex flex-col h-full shadow-lg">
|
||||||
{/* 头部 */}
|
{/* 头部 */}
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -306,7 +306,7 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'relative aspect-square rounded border cursor-pointer transition-all duration-200 overflow-hidden',
|
'relative aspect-square rounded border cursor-pointer transition-all duration-200 overflow-hidden',
|
||||||
selectedGenerationId === generation.id
|
selectedGenerationId === generation.id
|
||||||
? 'border-yellow-400'
|
? 'border-yellow-400 ring-2 ring-yellow-400/30'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -325,7 +325,8 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
if (asset.url) {
|
if (asset.url) {
|
||||||
setHoveredImage({
|
setHoveredImage({
|
||||||
url: asset.url,
|
url: asset.url,
|
||||||
title: `生成记录 G${index + 1}: ${generation.prompt.substring(0, 50)}${generation.prompt.length > 50 ? '...' : ''}`
|
title: `生成记录 G${index + 1}`,
|
||||||
|
description: generation.prompt
|
||||||
});
|
});
|
||||||
setPreviewPosition({x: e.clientX, y: e.clientY});
|
setPreviewPosition({x: e.clientX, y: e.clientY});
|
||||||
}
|
}
|
||||||
@@ -405,7 +406,7 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'relative aspect-square rounded border cursor-pointer transition-all duration-200 overflow-hidden',
|
'relative aspect-square rounded border cursor-pointer transition-all duration-200 overflow-hidden',
|
||||||
selectedEditId === edit.id
|
selectedEditId === edit.id
|
||||||
? 'border-purple-400'
|
? 'border-purple-400 ring-2 ring-purple-400/30'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -425,7 +426,8 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
if (asset.url) {
|
if (asset.url) {
|
||||||
setHoveredImage({
|
setHoveredImage({
|
||||||
url: asset.url,
|
url: asset.url,
|
||||||
title: `编辑记录 E${index + 1}: ${edit.instruction.substring(0, 50)}${edit.instruction.length > 50 ? '...' : ''}`
|
title: `编辑记录 E${index + 1}`,
|
||||||
|
description: edit.instruction
|
||||||
});
|
});
|
||||||
setPreviewPosition({x: e.clientX, y: e.clientY});
|
setPreviewPosition({x: e.clientX, y: e.clientY});
|
||||||
}
|
}
|
||||||
@@ -501,24 +503,7 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 当前图像信息 */}
|
|
||||||
{(canvasImage || imageDimensions) && (
|
|
||||||
<div className="mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
|
||||||
<h4 className="text-xs font-medium text-gray-500 mb-2">当前图像</h4>
|
|
||||||
<div className="space-y-1 text-xs text-gray-600">
|
|
||||||
{imageDimensions && (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>尺寸:</span>
|
|
||||||
<span className="text-gray-800">{imageDimensions.width} × {imageDimensions.height}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span>模式:</span>
|
|
||||||
<span className="text-gray-800 capitalize">{selectedTool}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 生成详情 */}
|
{/* 生成详情 */}
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
@@ -794,22 +779,35 @@ export const HistoryPanel: React.FC = () => {
|
|||||||
{/* 悬浮预览 */}
|
{/* 悬浮预览 */}
|
||||||
{hoveredImage && (
|
{hoveredImage && (
|
||||||
<div
|
<div
|
||||||
className="fixed z-50 shadow-2xl border-2 border-gray-300 rounded-lg overflow-hidden"
|
className="fixed z-50 shadow-2xl border border-gray-300 rounded-lg overflow-hidden bg-white backdrop-blur-sm"
|
||||||
style={{
|
style={{
|
||||||
left: Math.min(previewPosition.x + 10, window.innerWidth - 320),
|
left: Math.min(previewPosition.x + 10, window.innerWidth - 250),
|
||||||
top: Math.min(previewPosition.y + 10, window.innerHeight - 320),
|
top: Math.min(previewPosition.y + 10, window.innerHeight - 250),
|
||||||
maxWidth: '300px',
|
maxWidth: '250px',
|
||||||
maxHeight: '300px'
|
maxHeight: '250px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-black text-white text-xs p-1 truncate">
|
<div className="bg-gray-900 text-white text-xs p-2 truncate font-medium">
|
||||||
{hoveredImage.title}
|
{hoveredImage.title}
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
src={hoveredImage.url}
|
src={hoveredImage.url}
|
||||||
alt="预览"
|
alt="预览"
|
||||||
className="w-full h-full object-contain max-w-[300px] max-h-[300px]"
|
className="w-full h-auto max-h-[150px] object-contain"
|
||||||
/>
|
/>
|
||||||
|
{/* 图像信息 */}
|
||||||
|
<div className="p-2 bg-gray-50 border-t border-gray-200 text-xs">
|
||||||
|
{imageDimensions && (
|
||||||
|
<div className="flex justify-between text-gray-600">
|
||||||
|
<span>尺寸:</span>
|
||||||
|
<span className="text-gray-800">{imageDimensions.width} × {imageDimensions.height}</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ export const ImageCanvas: React.FC = () => {
|
|||||||
selectedTool,
|
selectedTool,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
brushSize,
|
brushSize,
|
||||||
setBrushSize
|
setBrushSize,
|
||||||
|
showHistory,
|
||||||
|
showPromptPanel
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
const stageRef = useRef<any>(null);
|
const stageRef = useRef<any>(null);
|
||||||
@@ -80,6 +82,22 @@ export const ImageCanvas: React.FC = () => {
|
|||||||
return () => window.removeEventListener('resize', updateSize);
|
return () => window.removeEventListener('resize', updateSize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 监听面板状态变化以调整画布大小
|
||||||
|
useEffect(() => {
|
||||||
|
// 使用 setTimeout 确保 DOM 已更新
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const container = document.getElementById('canvas-container');
|
||||||
|
if (container) {
|
||||||
|
setStageSize({
|
||||||
|
width: container.offsetWidth,
|
||||||
|
height: container.offsetHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [showPromptPanel, showHistory]);
|
||||||
|
|
||||||
// 处理鼠标滚轮缩放
|
// 处理鼠标滚轮缩放
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = document.getElementById('canvas-container');
|
const container = document.getElementById('canvas-container');
|
||||||
@@ -188,69 +206,7 @@ export const ImageCanvas: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<div className="p-2 border-b border-gray-200 bg-white">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{/* 左侧 - 缩放控制 */}
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<Button variant="outline" size="sm" onClick={() => handleZoom(-0.1)} className="h-8 w-8 p-0">
|
|
||||||
<ZoomOut className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<span className="text-xs text-gray-400 min-w-[40px] text-center">
|
|
||||||
{Math.round(canvasZoom * 100)}%
|
|
||||||
</span>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => handleZoom(0.1)} className="h-8 w-8 p-0">
|
|
||||||
<ZoomIn className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" onClick={handleReset} className="h-8 w-8 p-0">
|
|
||||||
<RotateCcw className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧 - 工具和操作 */}
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{selectedTool === 'mask' && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center space-x-1 mr-1">
|
|
||||||
<span className="text-xs text-gray-400">画笔:</span>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="5"
|
|
||||||
max="50"
|
|
||||||
value={brushSize}
|
|
||||||
onChange={(e) => setBrushSize(parseInt(e.target.value))}
|
|
||||||
className="w-12 h-2 bg-gray-800 rounded-lg appearance-none cursor-pointer slider"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-gray-400 w-6">{brushSize}</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearBrushStrokes}
|
|
||||||
disabled={brushStrokes.length === 0}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
<Eraser className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowMasks(!showMasks)}
|
|
||||||
className={cn(showMasks && 'bg-yellow-400/10 border-yellow-400/50', 'h-8 w-8 p-0')}
|
|
||||||
>
|
|
||||||
{showMasks ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{canvasImage && (
|
|
||||||
<Button variant="secondary" size="sm" onClick={handleDownload} className="h-8 px-2">
|
|
||||||
<Download className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 画布区域 */}
|
{/* 画布区域 */}
|
||||||
<div
|
<div
|
||||||
@@ -276,7 +232,7 @@ export const ImageCanvas: React.FC = () => {
|
|||||||
|
|
||||||
{isGenerating && (
|
{isGenerating && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/40 z-50 backdrop-blur-sm">
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/40 z-50 backdrop-blur-sm">
|
||||||
<div className="text-center bg-white/90 rounded-xl p-6 shadow-lg">
|
<div className="text-center bg-white/90 rounded-xl p-6 shadow-2xl backdrop-blur-sm border border-gray-200/50">
|
||||||
<div className="animate-spin rounded-full h-10 w-10 border-2 border-yellow-400 border-t-transparent mx-auto mb-3" />
|
<div className="animate-spin rounded-full h-10 w-10 border-2 border-yellow-400 border-t-transparent mx-auto mb-3" />
|
||||||
<p className="text-gray-700 text-sm font-medium">正在创建图像...</p>
|
<p className="text-gray-700 text-sm font-medium">正在创建图像...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -349,6 +305,48 @@ export const ImageCanvas: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
|
|
||||||
|
{/* 悬浮操作按钮 */}
|
||||||
|
{image && !isGenerating && (
|
||||||
|
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-white/80 backdrop-blur-sm rounded-full shadow-lg border border-gray-200 px-3 py-2 flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleZoom(-0.1)}
|
||||||
|
className="h-8 w-8 p-0 text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<ZoomOut className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-gray-500 min-w-[40px] text-center">
|
||||||
|
{Math.round(canvasZoom * 100)}%
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleZoom(0.1)}
|
||||||
|
className="h-8 w-8 p-0 text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<ZoomIn className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleReset}
|
||||||
|
className="h-8 w-8 p-0 text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<div className="w-px h-6 bg-gray-200"></div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="h-8 w-8 p-0 text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 状态栏 */}
|
{/* 状态栏 */}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export const PromptComposer: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-72 h-full bg-white border-r border-gray-200 p-4 flex flex-col space-y-5 overflow-y-auto">
|
<div className="w-72 h-full bg-white border-r border-gray-200 p-4 flex flex-col space-y-5 overflow-y-auto shadow-lg">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h3 className="text-xs font-medium text-gray-500 uppercase tracking-wide">模式</h3>
|
<h3 className="text-xs font-medium text-gray-500 uppercase tracking-wide">模式</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user