@@ -2257,7 +2421,7 @@ export function WorkflowCanvas() {
onEdgesChange={onEdgesChange}
onConnect={handleConnect}
onConnectEnd={handleConnectEnd}
- onMoveStart={() => { isPanningRef.current = true; setHoveredNodeId(null); document.documentElement.classList.add("canvas-interacting"); }}
+ onMoveStart={() => { isPanningRef.current = true; setHoveredNodeId?.(null); document.documentElement.classList.add("canvas-interacting"); }}
onMoveEnd={() => { isPanningRef.current = false; document.documentElement.classList.remove("canvas-interacting"); }}
onNodeDragStart={() => { isDraggingNodeRef.current = true; document.documentElement.classList.add("canvas-interacting"); }}
onNodeDragStop={(event, node) => { isDraggingNodeRef.current = false; document.documentElement.classList.remove("canvas-interacting"); handleNodeDragStop(event, node); }}
@@ -2297,6 +2461,7 @@ export function WorkflowCanvas() {
nodeClickDistance={5}
zoomOnScroll={tutorialActive ? false : false}
zoomOnPinch={tutorialActive ? false : !isModalOpen}
+ zoomOnDoubleClick={false}
minZoom={0.1}
maxZoom={4}
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
@@ -2562,6 +2727,14 @@ export function WorkflowCanvas() {
/>
)}
+ {canvasAddMenu && (
+
setCanvasAddMenu(null)}
+ />
+ )}
+
{/* Multi-select toolbar */}
diff --git a/src/components/__tests__/WorkflowCanvas.test.tsx b/src/components/__tests__/WorkflowCanvas.test.tsx
index 64ee37f0..16e1cbee 100644
--- a/src/components/__tests__/WorkflowCanvas.test.tsx
+++ b/src/components/__tests__/WorkflowCanvas.test.tsx
@@ -326,6 +326,26 @@ describe("WorkflowCanvas", () => {
});
});
+ describe("Canvas Add Node Menu", () => {
+ it("opens on canvas double click and creates a selected node", async () => {
+ render(
+
+
+
+ );
+
+ const pane = document.querySelector(".react-flow__pane") as HTMLElement;
+ fireEvent.doubleClick(pane, { clientX: 240, clientY: 260 });
+
+ expect(await screen.findByText("添加节点")).toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "图片" }));
+
+ expect(mockScreenToFlowPosition).toHaveBeenCalledWith({ x: 240, y: 260 });
+ expect(mockAddNode).toHaveBeenCalledWith("imageInput", { x: 240, y: 260 });
+ expect(screen.queryByText("添加节点")).not.toBeInTheDocument();
+ });
+ });
+
describe("Edge Types Registration", () => {
it("should register editable and reference edge types for the canvas", () => {
// Edge types are registered at module level
diff --git a/src/components/nodes/VideoInputNode.tsx b/src/components/nodes/VideoInputNode.tsx
index 525c6ea7..0833bdd1 100644
--- a/src/components/nodes/VideoInputNode.tsx
+++ b/src/components/nodes/VideoInputNode.tsx
@@ -14,7 +14,7 @@ import { readVideoFile } from "@/utils/videoFile";
type VideoInputNodeType = Node;
-// 空视频节点的快捷入口文案;使用转义写法,避免不同终端编码导致中文被写坏。
+// 空视频节点的快捷入口文案;使用转义写法,避免终端编码差异写坏中文。
const TRY_LABEL = "\u5c1d\u8bd5:";
const KEYFRAMES_ACTION_LABEL = "\u9996\u5c3e\u5e27\u751f\u6210\u89c6\u9891";
const FIRST_FRAME_ACTION_LABEL = "\u9996\u5e27\u751f\u6210\u89c6\u9891";
@@ -33,7 +33,7 @@ export function VideoInputNode({ id, data, selected }: NodeProps 视频节点”的链路,视频文件由用户后续在各节点上传。
+ // 只搭建“首帧/尾帧 -> 视频节点”的链路,视频文件由用户后续在各节点上传。
const createVideoNodeChain = useCallback(
(hasLastFrame: boolean) => {
const currentNode = nodes.find((node) => node.id === id);
@@ -47,7 +47,7 @@ export function VideoInputNode({ id, data, selected }: NodeProps
{nodeData.isOptional ? (
Optional