refine realtime canvas toolbar layout
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-03-08 15:14:12 +08:00
parent ecca6ff439
commit 250acaf1c5

View File

@ -18,7 +18,10 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Slider } from "@/components/ui/slider";
import { Textarea } from "@/components/ui/textarea";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { buildImageProxyUrl } from "@/lib/image-delivery";
import {
@ -35,6 +38,7 @@ import {
Upload,
Sparkles,
Save,
SlidersHorizontal,
Undo2,
Redo2,
Send,
@ -2431,6 +2435,34 @@ export function RealtimeCanvasDialog({
setStatusMessage("新建空白项目");
}, [clearPreviewCanvas, resetCanvas]);
const compactToolButtonClass =
"h-10 w-10 rounded-[18px] text-zinc-600 transition hover:bg-zinc-100 hover:text-zinc-900";
const compactToolButtonActiveClass = "bg-black text-white hover:bg-black hover:text-white";
const currentModeLabel =
brushMode === "select"
? "鼠标"
: brushMode === "brush"
? "画笔"
: brushMode === "eraser"
? "橡皮"
: brushMode === "rect"
? "矩形"
: brushMode === "circle"
? "圆形"
: "直线";
const currentModeDescription =
brushMode === "select"
? "选择图层并拖动位置"
: brushMode === "brush"
? "在画布上自由绘制"
: brushMode === "eraser"
? "擦除已有草图笔触"
: brushMode === "rect"
? "绘制矩形轮廓"
: brushMode === "circle"
? "绘制圆形轮廓"
: "绘制直线轮廓";
const content = (
<>
{standalone ? (
@ -2444,7 +2476,7 @@ export function RealtimeCanvasDialog({
<DialogDescription>左侧绘制草图,右侧实时预览。</DialogDescription>
</DialogHeader>
)}
<div className="absolute left-4 right-4 top-4 z-50 pointer-events-none">
<div className="hidden absolute left-4 right-4 top-4 z-50 pointer-events-none">
<div className="pointer-events-auto rounded-[24px] border border-black/10 bg-white/98 shadow-[0_12px_36px_rgba(15,23,42,0.1)]">
<div className="flex items-center gap-2 overflow-x-auto px-2 py-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div className="flex h-10 shrink-0 items-center gap-2 rounded-full border border-black/10 bg-zinc-50 px-3">
@ -3027,40 +3059,399 @@ export function RealtimeCanvasDialog({
className="hidden"
onChange={handleSketchUpload}
/>
<div className="absolute bottom-5 left-1/2 z-20 w-[min(760px,calc(100vw-2rem))] -translate-x-1/2 pointer-events-none">
<div className="pointer-events-auto flex items-center gap-2 rounded-[22px] border border-black/10 bg-white/96 px-2 py-2 shadow-[0_12px_40px_rgba(15,23,42,0.12)]">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
<Sparkles className="h-4 w-4" />
<div className="absolute bottom-5 left-1/2 z-30 flex w-[min(1100px,calc(100vw-2rem))] -translate-x-1/2 flex-col items-center gap-3 pointer-events-none">
<TooltipProvider delayDuration={120}>
<div className="pointer-events-auto flex max-w-full items-center gap-2 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div className="flex shrink-0 items-center gap-1 rounded-[28px] border border-black/10 bg-white/94 px-2 py-2 shadow-[0_14px_40px_rgba(15,23,42,0.14)]">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={cn(compactToolButtonClass, brushMode === "select" && compactToolButtonActiveClass)}
onClick={() => {
setSelectedLayerId(DRAW_LAYER_ID);
setBrushMode("select");
}}
>
<MousePointer2 className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">鼠标</div>
<div className="text-xs text-zinc-500">选择图层并拖动位置</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={cn(compactToolButtonClass, brushMode === "brush" && compactToolButtonActiveClass)}
onClick={() => {
setSelectedLayerId(DRAW_LAYER_ID);
setBrushMode("brush");
}}
>
<Paintbrush className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">画笔</div>
<div className="text-xs text-zinc-500">在画布上自由绘制</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={cn(compactToolButtonClass, brushMode === "eraser" && compactToolButtonActiveClass)}
onClick={() => {
setSelectedLayerId(DRAW_LAYER_ID);
setBrushMode("eraser");
}}
>
<Eraser className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">橡皮</div>
<div className="text-xs text-zinc-500">擦除当前草图笔触</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<Popover>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={cn(compactToolButtonClass, isShapeMode(brushMode) && compactToolButtonActiveClass)}
>
<Square className="h-4.5 w-4.5" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<PopoverContent className="w-[220px] rounded-[20px] border-black/10 bg-white/95 p-3 shadow-[0_16px_44px_rgba(15,23,42,0.14)]">
<div className="mb-2 text-xs font-medium text-zinc-500">形状工具</div>
<div className="grid grid-cols-3 gap-2">
<Button
type="button"
variant="ghost"
className={cn("h-12 rounded-2xl", brushMode === "rect" && "bg-black text-white hover:bg-black")}
onClick={() => {
setSelectedLayerId(DRAW_LAYER_ID);
setBrushMode("rect");
}}
>
<Square className="h-4.5 w-4.5" />
</Button>
<Button
type="button"
variant="ghost"
className={cn("h-12 rounded-2xl", brushMode === "circle" && "bg-black text-white hover:bg-black")}
onClick={() => {
setSelectedLayerId(DRAW_LAYER_ID);
setBrushMode("circle");
}}
>
<CircleIcon className="h-4.5 w-4.5" />
</Button>
<Button
type="button"
variant="ghost"
className={cn("h-12 rounded-2xl", brushMode === "line" && "bg-black text-white hover:bg-black")}
onClick={() => {
setSelectedLayerId(DRAW_LAYER_ID);
setBrushMode("line");
}}
>
<Slash className="h-4.5 w-4.5" />
</Button>
</div>
</PopoverContent>
</Popover>
<TooltipContent>
<div className="text-sm font-medium">形状</div>
<div className="text-xs text-zinc-500">矩形、圆形与直线</div>
</TooltipContent>
</Tooltip>
</div>
<div className="flex shrink-0 items-center gap-1 rounded-[28px] border border-black/10 bg-white/94 px-2 py-2 shadow-[0_14px_40px_rgba(15,23,42,0.14)]">
<Tooltip>
<Popover>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={compactToolButtonClass}
>
<SlidersHorizontal className="h-4.5 w-4.5" />
</Button>
</PopoverTrigger>
</TooltipTrigger>
<PopoverContent className="w-[260px] rounded-[20px] border-black/10 bg-white/95 p-4 shadow-[0_16px_44px_rgba(15,23,42,0.14)]">
<div className="space-y-4">
<div className="space-y-2">
<div className="text-xs font-medium text-zinc-500">颜色</div>
<button
type="button"
className="flex h-11 w-full items-center justify-between rounded-2xl border border-black/10 bg-white px-3 text-sm font-medium text-zinc-700"
onClick={() => colorInputRef.current?.click()}
>
<span>选择颜色</span>
<span
className="h-5 w-5 rounded-full border border-black/10"
style={{ backgroundColor: brushColor }}
/>
</button>
<input
ref={colorInputRef}
type="color"
value={brushColor}
onChange={(event) => setBrushColor(event.target.value)}
className="sr-only"
title="自定义颜色"
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-xs font-medium text-zinc-500">
<span>粗细</span>
<span className="w-8 text-right text-zinc-900">{brushSize}</span>
</div>
<Slider
value={[brushSize]}
min={2}
max={50}
step={1}
onValueChange={([val]) => setBrushSize(val)}
className="w-full"
trackClassName="h-2.5 bg-zinc-200 shadow-inner"
rangeClassName="bg-black"
thumbClassName="h-5 w-5 border border-zinc-300 bg-white shadow-sm"
/>
</div>
</div>
</PopoverContent>
</Popover>
<TooltipContent>
<div className="text-sm font-medium">画笔设置</div>
<div className="text-xs text-zinc-500">调整颜色和粗细</div>
</TooltipContent>
</Tooltip>
</div>
<div className="flex shrink-0 items-center gap-1 rounded-[28px] border border-black/10 bg-white/94 px-2 py-2 shadow-[0_14px_40px_rgba(15,23,42,0.14)]">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={compactToolButtonClass}
onClick={() => uploadInputRef.current?.click()}
>
<Upload className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">上传图片</div>
<div className="text-xs text-zinc-500">添加新的图片图层</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={compactToolButtonClass}
onClick={() => void saveCurrentProject()}
>
<Save className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">保存项目</div>
<div className="text-xs text-zinc-500">保存当前画布状态</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={compactToolButtonClass}
onClick={() => {
refreshSavedProjects();
setProjectPanelOpen((openState) => !openState);
}}
>
<FolderOpen className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">项目列表</div>
<div className="text-xs text-zinc-500">打开已保存项目</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={compactToolButtonClass}
onClick={startBlankProject}
>
<Plus className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">新建空白</div>
<div className="text-xs text-zinc-500">清空并开始新项目</div>
</TooltipContent>
</Tooltip>
</div>
<div className="flex shrink-0 items-center gap-1 rounded-[28px] border border-black/10 bg-white/94 px-2 py-2 shadow-[0_14px_40px_rgba(15,23,42,0.14)]">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={compactToolButtonClass}
onClick={undo}
disabled={!canUndo}
>
<Undo2 className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">撤销</div>
<div className="text-xs text-zinc-500">回退上一步编辑</div>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className={compactToolButtonClass}
onClick={redo}
disabled={!canRedo}
>
<Redo2 className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">重做</div>
<div className="text-xs text-zinc-500">恢复已撤销操作</div>
</TooltipContent>
</Tooltip>
{standalone ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
size="icon"
variant="ghost"
className="h-10 w-10 rounded-[18px] bg-black text-white transition hover:bg-black/90 hover:text-white"
onClick={() => onOpenChange(false)}
>
<X className="h-4.5 w-4.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="text-sm font-medium">关闭页面</div>
<div className="text-xs text-zinc-500">返回上一层画布</div>
</TooltipContent>
</Tooltip>
) : null}
</div>
</div>
<Input
</TooltipProvider>
<div className="pointer-events-auto w-full rounded-[34px] border border-black/10 bg-white/96 p-6 shadow-[0_20px_48px_rgba(15,23,42,0.16)]">
<Textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onBlur={() => scheduleFlush(true, false)}
onKeyDown={(event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
event.preventDefault();
triggerGenerate();
}
}}
placeholder="输入指令,基于当前画布实时生成"
className="h-9 flex-1 border-0 bg-transparent px-1 text-sm shadow-none focus-visible:ring-0"
className="min-h-[138px] resize-none border-0 bg-transparent p-0 text-base leading-8 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
/>
<Button
size="sm"
className="h-9 shrink-0 rounded-full bg-black px-4 font-medium text-white hover:bg-black/90"
onClick={triggerGenerate}
disabled={status === "connecting"}
>
{status === "connecting" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"生成"
)}
</Button>
<Button
size="sm"
variant="outline"
className="h-9 shrink-0 rounded-full px-4 font-medium"
onClick={handleApplyToCanvas}
disabled={!previewUrl}
>
<Send className="mr-1.5 h-4 w-4" />
发送到画布
</Button>
<div className="mt-5 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="flex flex-wrap items-center gap-2">
<span
className={cn(
"flex items-center gap-1.5 rounded-full px-3 py-2 text-xs font-medium",
statusBadge.className
)}
>
<span
className={cn(
"h-2 w-2 rounded-full",
status === "live"
? "bg-emerald-500 animate-pulse"
: status === "error"
? "bg-rose-500"
: "bg-zinc-400"
)}
/>
{statusBadge.label}
</span>
<span className="rounded-full bg-zinc-100 px-3 py-2 text-xs text-zinc-600">
延迟 {adaptiveThrottleMs}ms
</span>
<span className="rounded-full bg-black px-3 py-2 text-xs font-medium text-white">
当前工具 {currentModeLabel}
</span>
<span className="rounded-full bg-zinc-100 px-3 py-2 text-xs text-zinc-600">
{currentModeDescription}
</span>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
className="h-10 shrink-0 rounded-full bg-black px-5 font-medium text-white hover:bg-black/90"
onClick={triggerGenerate}
disabled={status === "connecting"}
>
{status === "connecting" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
"生成"
)}
</Button>
<Button
size="sm"
variant="outline"
className="h-10 shrink-0 rounded-full px-5 font-medium"
onClick={handleApplyToCanvas}
disabled={!previewUrl}
>
<Send className="mr-1.5 h-4 w-4" />
发送到画布
</Button>
</div>
</div>
</div>
</div>
</>
@ -3076,7 +3467,7 @@ export function RealtimeCanvasDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="h-[95vh] w-[98vw] max-w-[1800px] overflow-hidden rounded-[20px] border-0 bg-background p-0 shadow-2xl sm:rounded-[20px] md:rounded-[24px]">
<DialogContent className="h-[97vh] w-[98vw] max-w-[1800px] overflow-hidden rounded-[20px] border-0 bg-background p-0 shadow-2xl sm:rounded-[20px] md:rounded-[24px]">
{content}
</DialogContent>
</Dialog>