feat: comprehensive UI redesign for infinite canvas (LOVART style)
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-01-24 21:27:36 +08:00
parent f289eca90c
commit 79e5a870e1
2 changed files with 205 additions and 223 deletions

View File

@ -3,6 +3,7 @@
import { useEffect, useState, useCallback, use } from "react";
import { useRouter } from "next/navigation";
import {
ArrowLeft,
Save,
Undo2,
Redo2,
@ -360,12 +361,27 @@ export default function CanvasEditorPage({ params }: PageProps) {
return (
<TooltipProvider>
<div className="h-full flex flex-col bg-background">
{/* 顶部工具栏 */}
<div className="h-10 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">
{isEditingTitle ? (
<div className="relative h-dvh w-full overflow-hidden bg-background">
{/* 1. Canvas Layer (Full Screen) */}
<div className="absolute inset-0 z-0">
<CanvasWorkspace />
</div>
{/* 2. Floating Header (Top Left) */}
<div className="absolute top-4 left-4 z-20 flex items-center gap-2 bg-background/80 backdrop-blur-md border rounded-xl p-1.5 shadow-sm">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => router.push('/')} className="rounded-lg h-8 w-8">
<ArrowLeft className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>返回首页</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-5" />
{isEditingTitle ? (
<Input
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
@ -374,12 +390,12 @@ export default function CanvasEditorPage({ params }: PageProps) {
if (e.key === 'Enter') handleUpdateTitle();
if (e.key === 'Escape') setIsEditingTitle(false);
}}
className="h-7 w-48 text-sm"
className="h-8 w-48 text-sm bg-transparent border-none focus-visible:ring-0 px-2"
autoFocus
/>
) : (
<button
className="text-sm font-medium hover:text-primary transition-colors"
className="text-sm font-medium px-3 h-8 hover:bg-muted rounded-lg transition-colors"
onClick={() => {
setEditTitle(projectTitle);
setIsEditingTitle(true);
@ -388,208 +404,81 @@ export default function CanvasEditorPage({ params }: PageProps) {
{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">
{/* 左侧面板 - 标签页切换 AI对话/快捷工具 */}
<div
className={cn(
"border-r bg-card transition-all duration-300 ease-in-out overflow-hidden flex flex-col",
isPanelOpen ? "w-96" : "w-0"
)}
>
{isPanelOpen && (
{/* 3. Floating Actions (Top Right) */}
<div className="absolute top-4 right-4 z-20 flex items-center gap-2">
<div className="bg-background/80 backdrop-blur-md border rounded-xl p-1.5 shadow-sm flex items-center gap-1">
{isGenerating && (
<div className="flex items-center gap-1.5 text-xs text-muted-foreground px-2 border-r mr-1">
<Loader2 className="h-3 w-3 animate-spin" />
<span className="hidden sm:inline">生成中...</span>
</div>
)}
{/* Save Button */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handleSave}
disabled={isSaving}
className="rounded-lg h-8"
>
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
<span className="ml-2 hidden sm:inline">保存</span>
</Button>
</TooltipTrigger>
<TooltipContent>保存 (Ctrl+S)</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-5" />
{/* Panel Toggle */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => setIsPanelOpen(!isPanelOpen)}
className={cn("rounded-lg h-8 w-8", isPanelOpen && "bg-muted")}
>
{isPanelOpen ? <PanelLeftClose className="h-4 w-4" /> : <PanelLeft className="h-4 w-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>{isPanelOpen ? "隐藏面板" : "显示面板"}</TooltipContent>
</Tooltip>
</div>
</div>
{/* 4. Floating Sidebar (Left) */}
<div className={cn(
"absolute top-20 left-4 bottom-24 z-10 w-[400px] transition-all duration-300 ease-in-out",
isPanelOpen ? "translate-x-0 opacity-100" : "-translate-x-[120%] opacity-0"
)}>
<div className="h-full w-full rounded-2xl border bg-background/95 backdrop-blur-md shadow-2xl overflow-hidden flex flex-col">
<Tabs defaultValue="chat" className="flex flex-col h-full">
<TabsList className="grid w-full grid-cols-2 shrink-0 m-2 mb-0" style={{ width: 'calc(100% - 16px)' }}>
<TabsTrigger value="chat" className="text-xs">
<MessageSquare className="h-3.5 w-3.5 mr-1.5" />
AI 对话
</TabsTrigger>
<TabsTrigger value="tools" className="text-xs">
<Wand2 className="h-3.5 w-3.5 mr-1.5" />
快捷工具
</TabsTrigger>
</TabsList>
<div className="px-4 pt-4 pb-2 bg-muted/30 border-b">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="chat" className="text-xs">
<MessageSquare className="h-3.5 w-3.5 mr-1.5" />
AI 对话
</TabsTrigger>
<TabsTrigger value="tools" className="text-xs">
<Wand2 className="h-3.5 w-3.5 mr-1.5" />
快捷工具
</TabsTrigger>
</TabsList>
</div>
{/* AI 对话标签页 */}
<TabsContent value="chat" className="flex-1 overflow-hidden m-0 mt-2">
<TabsContent value="chat" className="flex-1 overflow-hidden m-0">
<CanvasChatPanel chatId={`canvas-${id}`} />
</TabsContent>
{/* 快捷工具标签页 */}
<TabsContent value="tools" className="flex-1 overflow-hidden m-0 mt-2">
<TabsContent value="tools" className="flex-1 overflow-hidden m-0">
<ScrollArea className="h-full">
<div className="p-4 space-y-6">
{/* AI 图片生成 */}
@ -606,7 +495,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
placeholder="描述你想要生成的图片..."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="min-h-[100px] text-sm resize-none"
className="min-h-[100px] text-sm resize-none bg-muted/30"
/>
</div>
@ -616,14 +505,14 @@ export default function CanvasEditorPage({ params }: PageProps) {
placeholder="描述你不想出现的内容..."
value={negativePrompt}
onChange={(e) => setNegativePrompt(e.target.value)}
className="min-h-[60px] text-sm resize-none"
className="min-h-[60px] text-sm resize-none bg-muted/30"
/>
</div>
<div className="space-y-2">
<Label className="text-xs">尺寸比例</Label>
<Select value={aspectRatio} onValueChange={setAspectRatio}>
<SelectTrigger className="h-9">
<SelectTrigger className="h-9 bg-muted/30">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -681,33 +570,33 @@ export default function CanvasEditorPage({ params }: PageProps) {
{selectedImageNode ? (
<div className="grid grid-cols-2 gap-2">
<Button variant="outline" size="sm" className="h-9 justify-start">
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30">
<Scissors className="h-4 w-4 mr-2" />
抠图
</Button>
<Button variant="outline" size="sm" className="h-9 justify-start">
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30">
<Sparkles className="h-4 w-4 mr-2" />
变清晰
</Button>
<Button variant="outline" size="sm" className="h-9 justify-start">
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30">
<Expand className="h-4 w-4 mr-2" />
扩展
</Button>
<Button variant="outline" size="sm" className="h-9 justify-start">
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30">
<Maximize2 className="h-4 w-4 mr-2" />
调整
</Button>
<Button variant="outline" size="sm" className="h-9 justify-start">
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30">
<Move3D className="h-4 w-4 mr-2" />
角度
</Button>
<Button variant="outline" size="sm" className="h-9 justify-start">
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30">
<Layers className="h-4 w-4 mr-2" />
场景
</Button>
</div>
) : (
<p className="text-xs text-muted-foreground text-center py-4">
<p className="text-xs text-muted-foreground text-center py-4 bg-muted/20 rounded-lg border border-dashed">
选中图片节点以使用编辑工具
</p>
)}
@ -723,19 +612,19 @@ export default function CanvasEditorPage({ params }: PageProps) {
</div>
<div className="grid grid-cols-2 gap-2">
<Button variant="outline" size="sm" className="h-9 justify-start" onClick={handleAddImageNode}>
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30" onClick={handleAddImageNode}>
<ImagePlus className="h-4 w-4 mr-2" />
添加图片
</Button>
<Button variant="outline" size="sm" className="h-9 justify-start" onClick={handleAddTextNode}>
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30" onClick={handleAddTextNode}>
<Type className="h-4 w-4 mr-2" />
添加文本
</Button>
<Button variant="outline" size="sm" className="h-9 justify-start">
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30">
<Upload className="h-4 w-4 mr-2" />
上传图片
</Button>
<Button variant="outline" size="sm" className="h-9 justify-start">
<Button variant="outline" size="sm" className="h-9 justify-start bg-muted/30">
<Download className="h-4 w-4 mr-2" />
导出
</Button>
@ -745,14 +634,107 @@ export default function CanvasEditorPage({ params }: PageProps) {
</ScrollArea>
</TabsContent>
</Tabs>
)}
</div>
{/* 画布工作区 */}
<div className="flex-1 overflow-hidden bg-muted/30">
<CanvasWorkspace />
</div>
</div>
</div>
{/* 5. Floating Bottom Toolbar (Center) */}
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-20">
<div className="flex items-center gap-1.5 p-2 rounded-2xl border bg-background/80 backdrop-blur-md shadow-2xl">
{/* Tools: Select, Pan, Add Text, Add Image, Add Video */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={tool === 'select' ? 'secondary' : 'ghost'}
size="icon"
onClick={() => setTool('select')}
className={cn("rounded-xl h-10 w-10", tool === 'select' && "bg-primary text-primary-foreground hover:bg-primary/90")}
>
<MousePointer className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>选择 (V)</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={tool === 'pan' ? 'secondary' : 'ghost'}
size="icon"
onClick={() => setTool('pan')}
className={cn("rounded-xl h-10 w-10", tool === 'pan' && "bg-primary text-primary-foreground hover:bg-primary/90")}
>
<Hand className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>平移 (H)</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-6 mx-1" />
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={handleAddTextNode} className="rounded-xl h-10 w-10">
<Type className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>添加文本</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={handleAddImageNode} className="rounded-xl h-10 w-10">
<ImagePlus className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>添加图片</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={handleAddVideoNode} className="rounded-xl h-10 w-10">
<Video className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>添加视频</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-6 mx-1" />
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={undo} disabled={!canUndo} className="rounded-xl h-10 w-10">
<Undo2 className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>撤销</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={redo} disabled={!canRedo} className="rounded-xl h-10 w-10">
<Redo2 className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>重做</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="h-6 mx-1" />
{/* Zoom Controls */}
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" onClick={zoomOut} className="h-8 w-8 rounded-lg">
<ZoomOut className="h-4 w-4" />
</Button>
<span className="text-xs w-10 text-center tabular-nums font-medium">
{Math.round(viewport.zoom * 100)}%
</span>
<Button variant="ghost" size="icon" onClick={zoomIn} className="h-8 w-8 rounded-lg">
<ZoomIn className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
</TooltipProvider>
);

View File

@ -217,7 +217,7 @@ export function CanvasWorkspace() {
<div
ref={containerRef}
className={cn(
"relative w-full h-full overflow-hidden bg-[#f5f5f5] dark:bg-[#1a1a2e]",
"relative w-full h-full overflow-hidden bg-background",
tool === 'pan' && "cursor-grab",
isPanning && "cursor-grabbing"
)}