diff --git a/popiart-node-interaction-enhancement-implementation.md b/popiart-node-interaction-enhancement-implementation.md index 98ee274b..ab58c2e4 100644 --- a/popiart-node-interaction-enhancement-implementation.md +++ b/popiart-node-interaction-enhancement-implementation.md @@ -2044,3 +2044,15 @@ npm.cmd run test:run -- VideoInputNode.test.tsx WorkflowCanvas.test.tsx ImageNod ``` 结果:5 个测试文件、87 个测试通过。 +## 2026-04-30 更新:首尾帧目标节点改为视频节点 + +本次调整“首尾帧生成视频”的建链目标: + +1. `首帧` 和 `尾帧` 仍然都是空的视频上传节点,用户后续自行上传视频。 +2. 右侧被连接的目标也改为空的 `videoInput` 视频节点,不再创建 `generateVideo` 节点。 +3. 自动连线保持 `video -> video`,形成 `首帧/尾帧 -> 视频节点` 的纯视频节点链路。 + +验证: +```powershell +npm.cmd run test:run -- VideoInputNode.test.tsx WorkflowCanvas.test.tsx +``` diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 60fada00..40226bea 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -2218,7 +2218,8 @@ export function WorkflowCanvas() { )} {/* Welcome Modal */} - {showQuickstart && ( + {/* 新建项目设置弹窗打开时不再同时挂载欢迎弹窗,避免两个弹窗状态互相触发。 */} + {showQuickstart && !showNewProjectSetup && ( { await loadWorkflow(workflow, directoryPath); diff --git a/src/components/__tests__/VideoInputNode.test.tsx b/src/components/__tests__/VideoInputNode.test.tsx index 997de9c0..898f4d8d 100644 --- a/src/components/__tests__/VideoInputNode.test.tsx +++ b/src/components/__tests__/VideoInputNode.test.tsx @@ -8,6 +8,7 @@ const KEYFRAMES_LABEL = "\u9996\u5c3e\u5e27\u751f\u6210\u89c6\u9891"; const FIRST_FRAME_LABEL = "\u9996\u5e27\u751f\u6210\u89c6\u9891"; const FIRST_FRAME_NODE_LABEL = "\u9996\u5e27"; const LAST_FRAME_NODE_LABEL = "\u5c3e\u5e27"; +const TARGET_VIDEO_NODE_LABEL = "\u89c6\u9891\u8282\u70b9"; const mockUpdateNodeData = vi.fn(); let addNodeSequence = 0; @@ -92,7 +93,7 @@ describe("VideoInputNode", () => { expect(screen.getByRole("button", { name: FIRST_FRAME_LABEL })).toBeInTheDocument(); }); - it("labels the current node as first frame and links it to a generate video node", () => { + it("labels the current node as first frame and links it to a target video node", () => { render( @@ -104,16 +105,20 @@ describe("VideoInputNode", () => { expect(mockUpdateNodeData).toHaveBeenCalledWith("video-1", { label: FIRST_FRAME_NODE_LABEL, }); - expect(mockAddNode).toHaveBeenCalledWith("generateVideo", { x: 600, y: 160 }); + expect(mockAddNode).toHaveBeenCalledWith( + "videoInput", + { x: 600, y: 160 }, + { label: TARGET_VIDEO_NODE_LABEL } + ); expect(mockOnConnect).toHaveBeenCalledWith({ source: "video-1", sourceHandle: "video", - target: "generateVideo-1", + target: "videoInput-1", targetHandle: "video", }); }); - it("creates an empty last-frame video node and links both videos to a generate video node", () => { + it("creates empty last-frame and target video nodes, then links both source videos", () => { render( @@ -130,17 +135,21 @@ describe("VideoInputNode", () => { { x: 120, y: 492 }, { label: LAST_FRAME_NODE_LABEL } ); - expect(mockAddNode).toHaveBeenCalledWith("generateVideo", { x: 600, y: 316 }); + expect(mockAddNode).toHaveBeenCalledWith( + "videoInput", + { x: 600, y: 326 }, + { label: TARGET_VIDEO_NODE_LABEL } + ); expect(mockOnConnect).toHaveBeenCalledWith({ source: "video-1", sourceHandle: "video", - target: "generateVideo-2", + target: "videoInput-2", targetHandle: "video", }); expect(mockOnConnect).toHaveBeenCalledWith({ source: "videoInput-1", sourceHandle: "video", - target: "generateVideo-2", + target: "videoInput-2", targetHandle: "video", }); }); diff --git a/src/components/nodes/VideoInputNode.tsx b/src/components/nodes/VideoInputNode.tsx index c42171f2..525c6ea7 100644 --- a/src/components/nodes/VideoInputNode.tsx +++ b/src/components/nodes/VideoInputNode.tsx @@ -20,6 +20,7 @@ const KEYFRAMES_ACTION_LABEL = "\u9996\u5c3e\u5e27\u751f\u6210\u89c6\u9891"; const FIRST_FRAME_ACTION_LABEL = "\u9996\u5e27\u751f\u6210\u89c6\u9891"; const FIRST_FRAME_LABEL = "\u9996\u5e27"; const LAST_FRAME_LABEL = "\u5c3e\u5e27"; +const TARGET_VIDEO_LABEL = "\u89c6\u9891\u8282\u70b9"; export function VideoInputNode({ id, data, selected }: NodeProps) { const nodeData = data; @@ -32,8 +33,8 @@ export function VideoInputNode({ id, data, selected }: NodeProps 生成视频”的节点链路,视频文件由用户后续在各节点上传。 - const createGenerateVideoChain = useCallback( + // 只负责搭建“首帧/尾帧 -> 视频节点”的链路,视频文件由用户后续在各节点上传。 + const createVideoNodeChain = useCallback( (hasLastFrame: boolean) => { const currentNode = nodes.find((node) => node.id === id); const currentWidth = @@ -43,7 +44,7 @@ export function VideoInputNode({ id, data, selected }: NodeProps { event.preventDefault(); event.stopPropagation(); - createGenerateVideoChain(true); + createVideoNodeChain(true); }} > @@ -213,7 +218,7 @@ export function VideoInputNode({ id, data, selected }: NodeProps { event.preventDefault(); event.stopPropagation(); - createGenerateVideoChain(false); + createVideoNodeChain(false); }} > diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 4b326e4b..8a438eb4 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -658,7 +658,10 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ }, setShowQuickstart: (show: boolean) => { - set({ showQuickstart: show }); + // 避免重复写入相同状态触发无意义渲染,降低弹窗回调形成循环更新的风险。 + set((state) => ( + state.showQuickstart === show ? state : { showQuickstart: show } + )); }, setHoveredNodeId: (id: string | null) => {