- Add canvas project and node database tables - Create canvas workspace with drag, resize, zoom and pan - Add ImageNode with background removal, enhance, extend, angle edit, scene fusion - Add VideoNode with playback controls - Add TextNode for text editing - Integrate AI image generation API (Gemini/Alibaba) - Implement viewport culling for performance optimization - Add canvas entry in top navigation bar
751 lines
26 KiB
TypeScript
751 lines
26 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, useCallback, use } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import {
|
||
ArrowLeft,
|
||
Save,
|
||
Undo2,
|
||
Redo2,
|
||
ZoomIn,
|
||
ZoomOut,
|
||
MousePointer,
|
||
Hand,
|
||
Plus,
|
||
ImagePlus,
|
||
Video,
|
||
Type,
|
||
Loader2,
|
||
Settings,
|
||
Trash2,
|
||
Scissors,
|
||
Sparkles,
|
||
Expand,
|
||
Maximize2,
|
||
Move3D,
|
||
Layers,
|
||
MoreHorizontal,
|
||
PanelLeftClose,
|
||
PanelLeft,
|
||
Download,
|
||
Upload,
|
||
Wand2,
|
||
Grid3X3,
|
||
} from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { Label } from "@/components/ui/label";
|
||
import {
|
||
Tooltip,
|
||
TooltipContent,
|
||
TooltipProvider,
|
||
TooltipTrigger,
|
||
} from "@/components/ui/tooltip";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Separator } from "@/components/ui/separator";
|
||
import { Slider } from "@/components/ui/slider";
|
||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||
import { toast } from "sonner";
|
||
import { useCanvasStore, canvasSelectors } from "@/store/canvas";
|
||
import { canvasService, imageGenerationService } from "@/services/canvas";
|
||
import { CanvasWorkspace } from "@/components/canvas";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
interface PageProps {
|
||
params: Promise<{ id: string }>;
|
||
}
|
||
|
||
// 图片尺寸预设
|
||
const SIZE_PRESETS = [
|
||
{ label: "1:1", value: "1:1", width: 1024, height: 1024 },
|
||
{ label: "3:2", value: "3:2", width: 1536, height: 1024 },
|
||
{ label: "2:3", value: "2:3", width: 1024, height: 1536 },
|
||
{ label: "4:3", value: "4:3", width: 1365, height: 1024 },
|
||
{ label: "3:4", value: "3:4", width: 1024, height: 1365 },
|
||
{ label: "16:9", value: "16:9", width: 1820, height: 1024 },
|
||
{ label: "9:16", value: "9:16", width: 1024, height: 1820 },
|
||
];
|
||
|
||
export default function CanvasEditorPage({ params }: PageProps) {
|
||
const { id } = use(params);
|
||
const router = useRouter();
|
||
|
||
// Store state
|
||
const projectTitle = useCanvasStore((state) => state.projectTitle);
|
||
const viewport = useCanvasStore(canvasSelectors.viewport);
|
||
const tool = useCanvasStore(canvasSelectors.tool);
|
||
const canUndo = useCanvasStore(canvasSelectors.canUndo);
|
||
const canRedo = useCanvasStore(canvasSelectors.canRedo);
|
||
const isGenerating = useCanvasStore(canvasSelectors.isGenerating);
|
||
const selectedNodeIds = useCanvasStore(canvasSelectors.selectedNodeIds);
|
||
const nodes = useCanvasStore(canvasSelectors.nodes);
|
||
|
||
// Store actions
|
||
const setProject = useCanvasStore((state) => state.setProject);
|
||
const setProjectTitle = useCanvasStore((state) => state.setProjectTitle);
|
||
const setTool = useCanvasStore((state) => state.setTool);
|
||
const zoomIn = useCanvasStore((state) => state.zoomIn);
|
||
const zoomOut = useCanvasStore((state) => state.zoomOut);
|
||
const undo = useCanvasStore((state) => state.undo);
|
||
const redo = useCanvasStore((state) => state.redo);
|
||
const loadCanvas = useCanvasStore((state) => state.loadCanvas);
|
||
const getCanvasState = useCanvasStore((state) => state.getCanvasState);
|
||
const addNode = useCanvasStore((state) => state.addNode);
|
||
const updateNode = useCanvasStore((state) => state.updateNode);
|
||
const setGenerating = useCanvasStore((state) => state.setGenerating);
|
||
|
||
// Local state
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||
const [editTitle, setEditTitle] = useState("");
|
||
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
||
|
||
// 生成配置
|
||
const [prompt, setPrompt] = useState("");
|
||
const [negativePrompt, setNegativePrompt] = useState("");
|
||
const [aspectRatio, setAspectRatio] = useState("1:1");
|
||
const [imageCount, setImageCount] = useState(1);
|
||
|
||
// 获取选中的图片节点
|
||
const selectedImageNode = selectedNodeIds.length === 1
|
||
? nodes.find(n => n.id === selectedNodeIds[0] && n.type === 'image')
|
||
: null;
|
||
|
||
// 加载画布数据
|
||
useEffect(() => {
|
||
const loadProject = async () => {
|
||
setIsLoading(true);
|
||
const result = await canvasService.getProject(id);
|
||
|
||
if (result.success && result.data) {
|
||
setProject(id, result.data.title);
|
||
if (result.data.canvasData) {
|
||
loadCanvas(result.data.canvasData);
|
||
}
|
||
} else {
|
||
toast.error(result.error || "加载画布失败");
|
||
router.push("/canvas");
|
||
}
|
||
setIsLoading(false);
|
||
};
|
||
|
||
loadProject();
|
||
}, [id]);
|
||
|
||
// 保存画布
|
||
const handleSave = useCallback(async () => {
|
||
setIsSaving(true);
|
||
const canvasData = getCanvasState();
|
||
const result = await canvasService.saveCanvasState(id, canvasData);
|
||
|
||
if (result.success) {
|
||
toast.success("保存成功");
|
||
} else {
|
||
toast.error(result.error || "保存失败");
|
||
}
|
||
setIsSaving(false);
|
||
}, [id, getCanvasState]);
|
||
|
||
// 更新标题
|
||
const handleUpdateTitle = async () => {
|
||
if (!editTitle.trim() || editTitle === projectTitle) {
|
||
setIsEditingTitle(false);
|
||
return;
|
||
}
|
||
|
||
const result = await canvasService.updateProject(id, { title: editTitle.trim() });
|
||
if (result.success) {
|
||
setProjectTitle(editTitle.trim());
|
||
} else {
|
||
toast.error(result.error || "更新标题失败");
|
||
}
|
||
setIsEditingTitle(false);
|
||
};
|
||
|
||
// 添加文本节点
|
||
const handleAddTextNode = () => {
|
||
addNode({
|
||
type: 'text',
|
||
position: { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 },
|
||
size: { width: 200, height: 100 },
|
||
data: { content: '双击编辑文本' },
|
||
connections: [],
|
||
});
|
||
};
|
||
|
||
// 添加图片节点
|
||
const handleAddImageNode = () => {
|
||
addNode({
|
||
type: 'image',
|
||
position: { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 },
|
||
size: { width: 300, height: 300 },
|
||
data: {
|
||
src: '',
|
||
status: 'pending',
|
||
prompt: '',
|
||
},
|
||
connections: [],
|
||
});
|
||
};
|
||
|
||
// 添加视频节点
|
||
const handleAddVideoNode = () => {
|
||
addNode({
|
||
type: 'video',
|
||
position: { x: 100 + Math.random() * 200, y: 100 + Math.random() * 200 },
|
||
size: { width: 400, height: 300 },
|
||
data: {
|
||
src: '',
|
||
status: 'pending',
|
||
prompt: '',
|
||
},
|
||
connections: [],
|
||
});
|
||
};
|
||
|
||
// AI 生成图片
|
||
const handleGenerate = async () => {
|
||
if (!prompt.trim()) {
|
||
toast.error("请输入提示词");
|
||
return;
|
||
}
|
||
|
||
setGenerating(true);
|
||
|
||
// 获取尺寸预设
|
||
const sizePreset = SIZE_PRESETS.find(s => s.value === aspectRatio) || SIZE_PRESETS[0];
|
||
|
||
// 先创建占位节点(显示生成中状态)
|
||
const placeholderNodeIds: string[] = [];
|
||
for (let i = 0; i < imageCount; i++) {
|
||
const nodeId = addNode({
|
||
type: 'image',
|
||
position: {
|
||
x: 100 + (i % 3) * 320,
|
||
y: 100 + Math.floor(i / 3) * 320
|
||
},
|
||
size: { width: 300, height: 300 },
|
||
data: {
|
||
src: '',
|
||
status: 'generating',
|
||
prompt: prompt,
|
||
},
|
||
connections: [],
|
||
});
|
||
placeholderNodeIds.push(nodeId);
|
||
}
|
||
|
||
try {
|
||
// 调用 AI 图片生成 API
|
||
const result = await imageGenerationService.generateImage({
|
||
prompt,
|
||
negativePrompt,
|
||
aspectRatio,
|
||
imageCount,
|
||
});
|
||
|
||
if (result.success && result.data?.images) {
|
||
// 更新节点为生成的图片
|
||
result.data.images.forEach((image, index) => {
|
||
if (placeholderNodeIds[index]) {
|
||
updateNode(placeholderNodeIds[index], {
|
||
data: {
|
||
src: image.originalUrl || image.thumbnailUrl,
|
||
status: 'completed',
|
||
prompt: prompt,
|
||
},
|
||
});
|
||
}
|
||
});
|
||
|
||
toast.success(`成功生成 ${result.data.images.length} 张图片`);
|
||
setPrompt(""); // 清空提示词
|
||
} else {
|
||
// 生成失败,更新节点状态
|
||
placeholderNodeIds.forEach(nodeId => {
|
||
updateNode(nodeId, {
|
||
data: {
|
||
src: '',
|
||
status: 'failed',
|
||
prompt: prompt,
|
||
},
|
||
});
|
||
});
|
||
toast.error(result.error || "图片生成失败");
|
||
}
|
||
} catch (error) {
|
||
console.error("AI 图片生成失败:", error);
|
||
// 生成失败,更新节点状态
|
||
placeholderNodeIds.forEach(nodeId => {
|
||
updateNode(nodeId, {
|
||
data: {
|
||
src: '',
|
||
status: 'failed',
|
||
prompt: prompt,
|
||
},
|
||
});
|
||
});
|
||
toast.error("图片生成失败,请重试");
|
||
} finally {
|
||
setGenerating(false);
|
||
}
|
||
};
|
||
|
||
// 键盘快捷键
|
||
useEffect(() => {
|
||
const handleKeyDown = (e: KeyboardEvent) => {
|
||
// 如果正在输入则不触发快捷键
|
||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
|
||
return;
|
||
}
|
||
|
||
// Ctrl/Cmd + S 保存
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||
e.preventDefault();
|
||
handleSave();
|
||
}
|
||
// Ctrl/Cmd + Z 撤销
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
undo();
|
||
}
|
||
// Ctrl/Cmd + Shift + Z 重做
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) {
|
||
e.preventDefault();
|
||
redo();
|
||
}
|
||
// V 选择工具
|
||
if (e.key === 'v' && !e.ctrlKey && !e.metaKey) {
|
||
setTool('select');
|
||
}
|
||
// H 平移工具
|
||
if (e.key === 'h' && !e.ctrlKey && !e.metaKey) {
|
||
setTool('pan');
|
||
}
|
||
// Delete 删除选中
|
||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||
useCanvasStore.getState().deleteSelectedNodes();
|
||
}
|
||
};
|
||
|
||
window.addEventListener('keydown', handleKeyDown);
|
||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||
}, [handleSave, undo, redo, setTool]);
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="h-full flex items-center justify-center bg-muted/30">
|
||
<div className="flex flex-col items-center gap-3">
|
||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||
<span className="text-sm text-muted-foreground">加载中...</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<TooltipProvider>
|
||
<div className="h-full flex flex-col bg-background">
|
||
{/* 顶部工具栏 */}
|
||
<div className="h-12 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex items-center justify-between px-4 shrink-0">
|
||
{/* 左侧:返回 + 标题 */}
|
||
<div className="flex items-center gap-2">
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button variant="ghost" size="icon" asChild>
|
||
<Link href="/canvas">
|
||
<ArrowLeft className="h-4 w-4" />
|
||
</Link>
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>返回列表</TooltipContent>
|
||
</Tooltip>
|
||
|
||
{isEditingTitle ? (
|
||
<Input
|
||
value={editTitle}
|
||
onChange={(e) => setEditTitle(e.target.value)}
|
||
onBlur={handleUpdateTitle}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') handleUpdateTitle();
|
||
if (e.key === 'Escape') setIsEditingTitle(false);
|
||
}}
|
||
className="h-8 w-48"
|
||
autoFocus
|
||
/>
|
||
) : (
|
||
<button
|
||
className="text-sm font-medium hover:text-primary transition-colors"
|
||
onClick={() => {
|
||
setEditTitle(projectTitle);
|
||
setIsEditingTitle(true);
|
||
}}
|
||
>
|
||
{projectTitle}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 中间:工具栏 */}
|
||
<div className="flex items-center gap-1">
|
||
{/* 选择/平移工具 */}
|
||
<div className="flex items-center bg-muted rounded-md p-0.5">
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
variant={tool === 'select' ? 'secondary' : 'ghost'}
|
||
size="icon"
|
||
className="h-7 w-7"
|
||
onClick={() => setTool('select')}
|
||
>
|
||
<MousePointer className="h-4 w-4" />
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>选择工具 (V)</TooltipContent>
|
||
</Tooltip>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
variant={tool === 'pan' ? 'secondary' : 'ghost'}
|
||
size="icon"
|
||
className="h-7 w-7"
|
||
onClick={() => setTool('pan')}
|
||
>
|
||
<Hand className="h-4 w-4" />
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>平移工具 (H)</TooltipContent>
|
||
</Tooltip>
|
||
</div>
|
||
|
||
<Separator orientation="vertical" className="h-6 mx-2" />
|
||
|
||
{/* 撤销/重做 */}
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8"
|
||
onClick={undo}
|
||
disabled={!canUndo}
|
||
>
|
||
<Undo2 className="h-4 w-4" />
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>撤销 (Ctrl+Z)</TooltipContent>
|
||
</Tooltip>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8"
|
||
onClick={redo}
|
||
disabled={!canRedo}
|
||
>
|
||
<Redo2 className="h-4 w-4" />
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>重做 (Ctrl+Shift+Z)</TooltipContent>
|
||
</Tooltip>
|
||
|
||
<Separator orientation="vertical" className="h-6 mx-2" />
|
||
|
||
{/* 缩放 */}
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8"
|
||
onClick={zoomOut}
|
||
>
|
||
<ZoomOut className="h-4 w-4" />
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>缩小</TooltipContent>
|
||
</Tooltip>
|
||
<span className="text-xs w-12 text-center tabular-nums">
|
||
{Math.round(viewport.zoom * 100)}%
|
||
</span>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8"
|
||
onClick={zoomIn}
|
||
>
|
||
<ZoomIn className="h-4 w-4" />
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>放大</TooltipContent>
|
||
</Tooltip>
|
||
|
||
<Separator orientation="vertical" className="h-6 mx-2" />
|
||
|
||
{/* 添加节点 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="outline" size="sm" className="h-8">
|
||
<Plus className="h-4 w-4 mr-1" />
|
||
添加
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center">
|
||
<DropdownMenuItem onClick={handleAddTextNode}>
|
||
<Type className="h-4 w-4 mr-2" />
|
||
文本节点
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={handleAddImageNode}>
|
||
<ImagePlus className="h-4 w-4 mr-2" />
|
||
图片节点
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={handleAddVideoNode}>
|
||
<Video className="h-4 w-4 mr-2" />
|
||
视频节点
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
|
||
{/* 右侧:面板切换 + 保存 */}
|
||
<div className="flex items-center gap-2">
|
||
{isGenerating && (
|
||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||
<Loader2 className="h-3 w-3 animate-spin" />
|
||
生成中...
|
||
</div>
|
||
)}
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8"
|
||
onClick={() => setIsPanelOpen(!isPanelOpen)}
|
||
>
|
||
{isPanelOpen ? (
|
||
<PanelLeftClose className="h-4 w-4" />
|
||
) : (
|
||
<PanelLeft className="h-4 w-4" />
|
||
)}
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>{isPanelOpen ? "隐藏面板" : "显示面板"}</TooltipContent>
|
||
</Tooltip>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
variant="default"
|
||
size="sm"
|
||
className="h-8"
|
||
onClick={handleSave}
|
||
disabled={isSaving}
|
||
>
|
||
{isSaving ? (
|
||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||
) : (
|
||
<Save className="h-4 w-4 mr-1" />
|
||
)}
|
||
保存
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>保存 (Ctrl+S)</TooltipContent>
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 主体区域 */}
|
||
<div className="flex-1 flex overflow-hidden">
|
||
{/* 左侧配置面板 - 参考 lobe-chat-pro */}
|
||
<div
|
||
className={cn(
|
||
"border-r bg-card transition-all duration-300 ease-in-out overflow-hidden",
|
||
isPanelOpen ? "w-80" : "w-0"
|
||
)}
|
||
>
|
||
{isPanelOpen && (
|
||
<ScrollArea className="h-full">
|
||
<div className="p-4 space-y-6">
|
||
{/* AI 图片生成 */}
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2">
|
||
<Wand2 className="h-4 w-4 text-primary" />
|
||
<h3 className="font-semibold text-sm">AI 图片生成</h3>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<div className="space-y-2">
|
||
<Label className="text-xs">提示词</Label>
|
||
<Textarea
|
||
placeholder="描述你想要生成的图片..."
|
||
value={prompt}
|
||
onChange={(e) => setPrompt(e.target.value)}
|
||
className="min-h-[100px] text-sm resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-xs">负面提示词</Label>
|
||
<Textarea
|
||
placeholder="描述你不想出现的内容..."
|
||
value={negativePrompt}
|
||
onChange={(e) => setNegativePrompt(e.target.value)}
|
||
className="min-h-[60px] text-sm resize-none"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-xs">尺寸比例</Label>
|
||
<Select value={aspectRatio} onValueChange={setAspectRatio}>
|
||
<SelectTrigger className="h-9">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{SIZE_PRESETS.map((preset) => (
|
||
<SelectItem key={preset.value} value={preset.value}>
|
||
{preset.label} ({preset.width}×{preset.height})
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-xs">生成数量</Label>
|
||
<span className="text-xs text-muted-foreground">{imageCount}</span>
|
||
</div>
|
||
<Slider
|
||
value={[imageCount]}
|
||
onValueChange={([v]) => setImageCount(v)}
|
||
min={1}
|
||
max={4}
|
||
step={1}
|
||
/>
|
||
</div>
|
||
|
||
<Button
|
||
className="w-full"
|
||
onClick={handleGenerate}
|
||
disabled={isGenerating || !prompt.trim()}
|
||
>
|
||
{isGenerating ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||
生成中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Sparkles className="h-4 w-4 mr-2" />
|
||
开始生成
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
{/* 图片编辑工具 */}
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2">
|
||
<Settings className="h-4 w-4 text-primary" />
|
||
<h3 className="font-semibold text-sm">图片编辑工具</h3>
|
||
</div>
|
||
|
||
{selectedImageNode ? (
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<Button variant="outline" size="sm" className="h-9 justify-start">
|
||
<Scissors className="h-4 w-4 mr-2" />
|
||
抠图
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="h-9 justify-start">
|
||
<Sparkles className="h-4 w-4 mr-2" />
|
||
变清晰
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="h-9 justify-start">
|
||
<Expand className="h-4 w-4 mr-2" />
|
||
扩展
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="h-9 justify-start">
|
||
<Maximize2 className="h-4 w-4 mr-2" />
|
||
调整
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="h-9 justify-start">
|
||
<Move3D className="h-4 w-4 mr-2" />
|
||
角度
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="h-9 justify-start">
|
||
<Layers className="h-4 w-4 mr-2" />
|
||
场景
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<p className="text-xs text-muted-foreground text-center py-4">
|
||
选中图片节点以使用编辑工具
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
{/* 快捷操作 */}
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-2">
|
||
<Grid3X3 className="h-4 w-4 text-primary" />
|
||
<h3 className="font-semibold text-sm">快捷操作</h3>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<Button variant="outline" size="sm" className="h-9 justify-start" onClick={handleAddImageNode}>
|
||
<ImagePlus className="h-4 w-4 mr-2" />
|
||
添加图片
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="h-9 justify-start" onClick={handleAddTextNode}>
|
||
<Type className="h-4 w-4 mr-2" />
|
||
添加文本
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="h-9 justify-start">
|
||
<Upload className="h-4 w-4 mr-2" />
|
||
上传图片
|
||
</Button>
|
||
<Button variant="outline" size="sm" className="h-9 justify-start">
|
||
<Download className="h-4 w-4 mr-2" />
|
||
导出
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ScrollArea>
|
||
)}
|
||
</div>
|
||
|
||
{/* 画布工作区 */}
|
||
<div className="flex-1 overflow-hidden bg-muted/30">
|
||
<CanvasWorkspace />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</TooltipProvider>
|
||
);
|
||
}
|