diff --git a/web/src/components/canvas/canvas-create-menus.tsx b/web/src/components/canvas/canvas-create-menus.tsx
new file mode 100644
index 0000000..52e4085
--- /dev/null
+++ b/web/src/components/canvas/canvas-create-menus.tsx
@@ -0,0 +1,112 @@
+import { useEffect, useRef } from "react";
+import { ImageIcon, List, Music2, Settings2, Video, X } from "lucide-react";
+
+import { canvasThemes } from "@/lib/canvas-theme";
+import { useThemeStore } from "@/stores/use-theme-store";
+import { listNodeDefinitions, useNodeRegistryVersion } from "@/lib/canvas/node-registry";
+import { CanvasNodeType, type ConnectionHandle, type Position } from "@/types/canvas";
+
+export type PendingConnectionCreate = {
+ connection: ConnectionHandle;
+ position: Position;
+};
+
+export function ConnectionCreateMenu({
+ pending,
+ onCreate,
+ onClose,
+}: {
+ pending: PendingConnectionCreate;
+ onCreate: (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio) => void;
+ onClose: () => void;
+}) {
+ const theme = canvasThemes[useThemeStore((state) => state.theme)];
+ return (
+
event.stopPropagation()}
+ onPointerDown={(event) => event.stopPropagation()}
+ >
+
+
+ 引用该节点生成
+
+
+
+
+ } title="文本生成" description="脚本、广告词、品牌文案" onClick={() => onCreate(CanvasNodeType.Text)} />
+ } title="图片生成" onClick={() => onCreate(CanvasNodeType.Image)} />
+ } title="视频生成" onClick={() => onCreate(CanvasNodeType.Video)} />
+ } title="音频参考" onClick={() => onCreate(CanvasNodeType.Audio)} />
+ } title="配置节点" description="模型、尺寸、数量和输入顺序" onClick={() => onCreate(CanvasNodeType.Config)} />
+
+
+ );
+}
+
+export function ConnectionCreateOption({ theme, icon, title, description, onClick }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; icon: React.ReactNode; title: string; description?: string; onClick?: () => void }) {
+ return (
+
+ );
+}
+
+export 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 (
+ event.stopPropagation()}
+ >
+
+
+ 选择节点
+
+
+
+
+ {definitions.map((def) => (
+ onCreate(def.type)} />
+ ))}
+
+
+ );
+}
diff --git a/web/src/components/canvas/canvas-refresh-shell.tsx b/web/src/components/canvas/canvas-refresh-shell.tsx
new file mode 100644
index 0000000..62b4c93
--- /dev/null
+++ b/web/src/components/canvas/canvas-refresh-shell.tsx
@@ -0,0 +1,34 @@
+export function CanvasRefreshShell() {
+ return (
+
+
+
+
+ {Array.from({ length: 7 }).map((_, index) => (
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx
index 3ca1c4e..e89c4b5 100644
--- a/web/src/pages/canvas/project.tsx
+++ b/web/src/pages/canvas/project.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
-import { BookOpen, Bot, Group, Home, ImageIcon, Images, List, Menu, Music2, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video, X } from "lucide-react";
+import { BookOpen, Bot, Group, Home, Images, Menu, Plus, Redo2, Trash2, Undo2, Upload, Video } from "lucide-react";
import { saveAs } from "file-saver";
import { requestEdit, requestGeneration, requestImageQuestion, type AiTextMessage } from "@/services/api/image";
@@ -64,11 +64,13 @@ import {
resolveMetadataReferences,
sourceNodeReferenceImages,
} from "@/lib/canvas/canvas-generation-helpers";
-import { getNodeDefinition, isBuiltinNodeType as isBuiltinType, listNodeDefinitions, useNodeRegistryVersion } from "@/lib/canvas/node-registry";
+import { getNodeDefinition, isBuiltinNodeType as isBuiltinType, useNodeRegistryVersion } from "@/lib/canvas/node-registry";
import { buildNodeContext } from "@/lib/canvas/plugin-node-context";
import { ensurePluginsLoaded } from "@/lib/canvas/plugin-loader";
import { registerBuiltinNodes } from "@/components/canvas/nodes/builtin-nodes";
import { CanvasPluginManagerModal } from "@/components/canvas/canvas-plugin-manager-modal";
+import { CanvasRefreshShell } from "@/components/canvas/canvas-refresh-shell";
+import { ConnectionCreateMenu, NodeCreateMenu, type PendingConnectionCreate } from "@/components/canvas/canvas-create-menus";
import type { CanvasNodeToolbarItem, CanvasPluginAi, CanvasPluginHost } from "@/types/canvas-plugin";
import {
CanvasNodeType,
@@ -95,11 +97,6 @@ type CanvasClipboard = {
connections: CanvasConnection[];
};
-type PendingConnectionCreate = {
- connection: ConnectionHandle;
- position: Position;
-};
-
type ConnectionDropTarget = {
nodeId: string | null;
isNearNode: boolean;
@@ -148,141 +145,6 @@ export default function CanvasPage() {
return ;
}
-function CanvasRefreshShell() {
- return (
-
-
-
-
- {Array.from({ length: 7 }).map((_, index) => (
-
- ))}
-
-
-
-
-
-
- );
-}
-
-function ConnectionCreateMenu({
- pending,
- onCreate,
- onClose,
-}: {
- pending: PendingConnectionCreate;
- onCreate: (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio) => void;
- onClose: () => void;
-}) {
- const theme = canvasThemes[useThemeStore((state) => state.theme)];
- return (
- event.stopPropagation()}
- onPointerDown={(event) => event.stopPropagation()}
- >
-
-
- 引用该节点生成
-
-
-
-
- } title="文本生成" description="脚本、广告词、品牌文案" onClick={() => onCreate(CanvasNodeType.Text)} />
- } title="图片生成" onClick={() => onCreate(CanvasNodeType.Image)} />
- } title="视频生成" onClick={() => onCreate(CanvasNodeType.Video)} />
- } title="音频参考" onClick={() => onCreate(CanvasNodeType.Audio)} />
- } title="配置节点" description="模型、尺寸、数量和输入顺序" onClick={() => onCreate(CanvasNodeType.Config)} />
-
-
- );
-}
-
-function ConnectionCreateOption({ theme, icon, title, description, onClick }: { theme: (typeof canvasThemes)[keyof typeof canvasThemes]; icon: React.ReactNode; title: string; description?: string; onClick?: () => void }) {
- return (
-
- );
-}
-
-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 (
- event.stopPropagation()}
- >
-
-
- 选择节点
-
-
-
-
- {definitions.map((def) => (
- onCreate(def.type)} />
- ))}
-
-
- );
-}
-
function InfiniteCanvasPage() {
const { message, modal } = App.useApp();
// 订阅节点注册表版本,插件动态注册/卸载后驱动画布重渲染