feat: add WorkflowCanvasControls component for enhanced zoom and layout functionality

This commit is contained in:
mlogclub
2026-06-28 22:27:17 +08:00
parent 79b8c93bc9
commit 4a1595ae73
3 changed files with 107 additions and 91 deletions
@@ -0,0 +1,93 @@
"use client"
import type { ReactNode } from "react"
import { Maximize2Icon, MinusIcon, PlusIcon, SparklesIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
export function WorkflowCanvasControls({
zoomPercent,
onZoomIn,
onZoomOut,
onResetZoom,
onFitView,
onAutoLayout,
autoLayoutDisabled = false,
}: {
zoomPercent?: string
onZoomIn?: () => void
onZoomOut?: () => void
onResetZoom?: () => void
onFitView?: () => void
onAutoLayout?: () => void
autoLayoutDisabled?: boolean
}) {
return (
<div className="inline-flex h-9 items-center gap-1 rounded-md bg-background/95 p-1 shadow-sm backdrop-blur">
<CanvasControlButton label="缩小" onClick={onZoomOut}>
<MinusIcon className="size-3.5" />
</CanvasControlButton>
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 w-12 px-1 text-xs tabular-nums"
aria-label="重置为 100%"
disabled={!onResetZoom}
onClick={onResetZoom}
/>
}
>
{zoomPercent ?? "100%"}
</TooltipTrigger>
<TooltipContent> 100%</TooltipContent>
</Tooltip>
<CanvasControlButton label="放大" onClick={onZoomIn}>
<PlusIcon className="size-3.5" />
</CanvasControlButton>
<CanvasControlButton label="适配画布" onClick={onFitView}>
<Maximize2Icon className="size-3.5" />
</CanvasControlButton>
<CanvasControlButton label="自动布局" onClick={onAutoLayout} disabled={autoLayoutDisabled}>
<SparklesIcon className="size-3.5" />
</CanvasControlButton>
</div>
)
}
function CanvasControlButton({
label,
onClick,
disabled = false,
children,
}: {
label: string
onClick?: () => void
disabled?: boolean
children: ReactNode
}) {
return (
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-xs"
className="size-7"
aria-label={label}
disabled={disabled || !onClick}
onClick={onClick}
/>
}
>
{children}
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
)
}
@@ -3,18 +3,13 @@
import type { ReactNode } from "react"
import {
CheckCircle2Icon,
Maximize2Icon,
MinusIcon,
PlusIcon,
RotateCcwIcon,
SaveIcon,
SendIcon,
SparklesIcon,
Undo2Icon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import type { WorkflowDraftValidation } from "./workflow-utils"
@@ -28,13 +23,6 @@ export function WorkflowEditorToolbar({
undoDisabled = false,
onRedo,
redoDisabled = false,
onAutoLayout,
autoLayoutDisabled = false,
zoomPercent,
onZoomIn,
onZoomOut,
onResetZoom,
onFitView,
onRestoreDefault,
restoreDefaultDisabled = false,
onValidate,
@@ -52,13 +40,6 @@ export function WorkflowEditorToolbar({
undoDisabled?: boolean
onRedo?: () => void
redoDisabled?: boolean
onAutoLayout?: () => void
autoLayoutDisabled?: boolean
zoomPercent?: string
onZoomIn?: () => void
onZoomOut?: () => void
onResetZoom?: () => void
onFitView?: () => void
onRestoreDefault?: () => void
restoreDefaultDisabled?: boolean
onValidate?: () => void
@@ -69,7 +50,7 @@ export function WorkflowEditorToolbar({
publishDisabled?: boolean
}) {
return (
<div className="inline-flex min-h-10 w-fit max-w-full items-center gap-3 rounded-md border bg-background/95 px-2 py-1 shadow-sm backdrop-blur">
<div className="inline-flex min-h-10 w-fit max-w-full items-center gap-3 rounded-md bg-background/95 px-2 py-1 shadow-sm backdrop-blur">
<div className="flex min-w-0 shrink items-center gap-2">
<span
className={cn(
@@ -86,35 +67,6 @@ export function WorkflowEditorToolbar({
</div>
<div className="flex min-w-0 items-center gap-1 overflow-x-auto">
{toolbarExtra}
<div className="flex h-7 shrink-0 items-center rounded-md border bg-muted/30 p-0.5">
<WorkflowToolbarIconButton label="缩小" onClick={onZoomOut}>
<MinusIcon className="size-3.5" />
</WorkflowToolbarIconButton>
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="ghost"
size="sm"
className="h-6 w-12 px-1 text-xs tabular-nums"
aria-label="重置为 100%"
disabled={!onResetZoom}
onClick={onResetZoom}
/>
}
>
{zoomPercent ?? "100%"}
</TooltipTrigger>
<TooltipContent> 100%</TooltipContent>
</Tooltip>
<WorkflowToolbarIconButton label="放大" onClick={onZoomIn}>
<PlusIcon className="size-3.5" />
</WorkflowToolbarIconButton>
<WorkflowToolbarIconButton label="适配画布" onClick={onFitView}>
<Maximize2Icon className="size-3.5" />
</WorkflowToolbarIconButton>
</div>
<Button type="button" variant="ghost" size="sm" className="h-7 shrink-0 px-2 text-xs" disabled={undoDisabled} onClick={onUndo}>
<Undo2Icon className="size-3.5" />
@@ -122,10 +74,6 @@ export function WorkflowEditorToolbar({
<Button type="button" variant="ghost" size="sm" className="h-7 shrink-0 px-2 text-xs" disabled={redoDisabled} onClick={onRedo}>
</Button>
<Button type="button" variant="ghost" size="sm" className="h-7 shrink-0 px-2 text-xs" disabled={autoLayoutDisabled} onClick={onAutoLayout}>
<SparklesIcon className="size-3.5" />
</Button>
<Button type="button" variant="ghost" size="sm" className="h-7 shrink-0 px-2 text-xs" disabled={restoreDefaultDisabled} onClick={onRestoreDefault}>
<RotateCcwIcon className="size-3.5" />
@@ -145,34 +93,3 @@ export function WorkflowEditorToolbar({
</div>
)
}
function WorkflowToolbarIconButton({
label,
onClick,
children,
}: {
label: string
onClick?: () => void
children: ReactNode
}) {
return (
<Tooltip>
<TooltipTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-xs"
className="size-6"
aria-label={label}
disabled={!onClick}
onClick={onClick}
/>
}
>
{children}
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
)
}
@@ -25,6 +25,7 @@ import {
WorkflowBranchSelectionProvider,
type SelectedWorkflowBranch,
} from "./workflow-branch-selection"
import { WorkflowCanvasControls } from "./workflow-canvas-controls"
import { WorkflowConfigPanel } from "./workflow-config-sidebar"
import { WorkflowEditorToolbar } from "./workflow-editor-toolbar"
import {
@@ -341,13 +342,6 @@ function WorkflowEditorInner({
undoDisabled={undoDisabled}
onRedo={onRedo}
redoDisabled={redoDisabled}
onAutoLayout={() => void autoLayout()}
autoLayoutDisabled={autoLayouting || definition.nodes.length < 2}
zoomPercent={zoomPercent}
onZoomIn={() => playgroundTools.zoomin(true)}
onZoomOut={() => playgroundTools.zoomout(true)}
onResetZoom={resetZoom}
onFitView={() => playgroundTools.fitView(true)}
onRestoreDefault={onRestoreDefault}
restoreDefaultDisabled={restoreDefaultDisabled}
onValidate={onValidate}
@@ -359,6 +353,18 @@ function WorkflowEditorInner({
/>
</div>
<div className="absolute bottom-3 left-3 z-50">
<WorkflowCanvasControls
zoomPercent={zoomPercent}
onZoomIn={() => playgroundTools.zoomin(true)}
onZoomOut={() => playgroundTools.zoomout(true)}
onResetZoom={resetZoom}
onFitView={() => playgroundTools.fitView(true)}
onAutoLayout={() => void autoLayout()}
autoLayoutDisabled={autoLayouting || definition.nodes.length < 2}
/>
</div>
<WorkflowPortAddProvider onRequestAdd={openNodeMenuFromPort}>
<EditorRenderer className="h-full w-full" />
</WorkflowPortAddProvider>