From 8d3f24452453904cf4af29a6e69550634336de3e Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Wed, 15 Jul 2026 13:10:53 +0800 Subject: [PATCH] feat(toolbar): add extension node management and close on outside click --- web/src/components/canvas/canvas-toolbar.tsx | 80 ++++++++++++++++++- web/src/components/canvas/infinite-canvas.tsx | 7 +- web/src/pages/canvas/project.tsx | 11 +++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/web/src/components/canvas/canvas-toolbar.tsx b/web/src/components/canvas/canvas-toolbar.tsx index ba4d6a6..1462e3d 100644 --- a/web/src/components/canvas/canvas-toolbar.tsx +++ b/web/src/components/canvas/canvas-toolbar.tsx @@ -1,9 +1,10 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode, RefObject } from "react"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Button, Segmented, Switch } from "antd"; -import { CircleDot, Eraser, FolderOpen, Grid2x2, Group, Hand, Image as ImageIcon, Info, Moon, Music2, Palette, Redo2, Settings2, Square, Sun, Trash2, Type, Undo2, Upload, Video } from "lucide-react"; +import { CircleDot, Eraser, FolderOpen, Grid2x2, Group, Hand, Image as ImageIcon, Info, Moon, Music2, Palette, Puzzle, Redo2, Settings2, Square, Sun, Trash2, Type, Undo2, Upload, Video } from "lucide-react"; import { canvasThemes, type CanvasBackgroundMode, type CanvasColorTheme, type CanvasTheme } from "@/lib/canvas-theme"; +import { getNodePluginId, listNodeDefinitions, useNodeRegistryVersion } from "@/lib/canvas/node-registry"; import { useThemeStore } from "@/stores/use-theme-store"; import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler"; @@ -19,6 +20,7 @@ export function CanvasToolbar({ onAddText, onAddConfig, onAddGroup, + onAddExtensionNode, onUndo, onRedo, onUpload, @@ -40,6 +42,7 @@ export function CanvasToolbar({ onAddText: () => void; onAddConfig: () => void; onAddGroup: () => void; + onAddExtensionNode: (type: string) => void; onUndo: () => void; onRedo: () => void; onUpload: () => void; @@ -51,6 +54,7 @@ export function CanvasToolbar({ onOpenMyAssets: () => void; }) { const wrapRef = useRef(null); + const rootRef = useRef(null); const colorTheme = useThemeStore((state) => state.theme); const setTheme = useThemeStore((state) => state.setTheme); const theme = canvasThemes[colorTheme]; @@ -58,13 +62,31 @@ export function CanvasToolbar({ const [tipX, setTipX] = useState(0); const [appearanceOpen, setAppearanceOpen] = useState(false); const [panelX, setPanelX] = useState(0); + const [extensionsOpen, setExtensionsOpen] = useState(false); + const [extPanelX, setExtPanelX] = useState(0); + // 扩展(插件)节点,随注册表变化实时更新 + useNodeRegistryVersion(); + const extensionDefs = listNodeDefinitions().filter((def) => def.showInCreateMenu !== false && getNodePluginId(def.type) !== "builtin"); const dockStyle = { background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.toolbar.item, boxShadow: colorTheme === "dark" ? "0 18px 45px rgba(0,0,0,.32)" : "0 16px 40px rgba(28,25,23,.12)" }; const hoverStyle = { background: theme.toolbar.itemHover, color: theme.toolbar.activeText }; const activeStyle = { background: theme.toolbar.activeBg, color: theme.toolbar.activeText }; const tip = hovered ? toolLabel(hovered) : ""; + // 点击工具栏(含弹出面板)以外的地方,关闭弹出的扩展节点/画布外观面板 + useEffect(() => { + if (!extensionsOpen && !appearanceOpen) return; + const handlePointerDown = (event: PointerEvent) => { + if (rootRef.current && !rootRef.current.contains(event.target as Node)) { + setExtensionsOpen(false); + setAppearanceOpen(false); + } + }; + document.addEventListener("pointerdown", handlePointerDown, true); + return () => document.removeEventListener("pointerdown", handlePointerDown, true); + }, [extensionsOpen, appearanceOpen]); + return ( -
+
{tip ? : null}
@@ -95,6 +117,26 @@ export function CanvasToolbar({ + {extensionDefs.length ? ( + { + setExtPanelX(getTipX(wrapRef.current, event.currentTarget)); + setAppearanceOpen(false); + setExtensionsOpen((value) => !value); + }} + > + + + ) : null} @@ -114,6 +156,7 @@ export function CanvasToolbar({ onHover={setHovered} onClick={(event) => { setPanelX(getTipX(wrapRef.current, event.currentTarget)); + setExtensionsOpen(false); setAppearanceOpen((value) => !value); }} > @@ -133,6 +176,36 @@ export function CanvasToolbar({
+ {extensionsOpen && extensionDefs.length ? ( +
+
扩展节点
+
+ {extensionDefs.map((def) => ( + + ))} +
+
+ ) : null} + {appearanceOpen ? (
container.removeEventListener("wheel", preventWheelScroll); }, [containerRef]); diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index 63c9158..845c652 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -225,9 +225,19 @@ function ConnectionCreateOption({ theme, icon, title, description, onClick }: { function NodeCreateMenu({ position, onCreate, onClose }: { position: Position; onCreate: (type: string) => void; onClose: () => void }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; useNodeRegistryVersion(); + const menuRef = useRef(null); const definitions = listNodeDefinitions().filter((def) => def.showInCreateMenu !== false); + // 点击菜单外的空白处自动关闭 + useEffect(() => { + const handlePointerDown = (event: PointerEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) onClose(); + }; + document.addEventListener("pointerdown", handlePointerDown, true); + return () => document.removeEventListener("pointerdown", handlePointerDown, true); + }, [onClose]); return (
createNode(CanvasNodeType.Text)} onAddConfig={() => createNode(CanvasNodeType.Config)} onAddGroup={() => createNode(CanvasNodeType.Group)} + onAddExtensionNode={(type) => createNode(type)} onUndo={undoCanvas} onRedo={redoCanvas} onUpload={() => handleUploadRequest()}