@@ -70,14 +70,25 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
7070 if ( project . workflow ) {
7171 try {
7272 const flow = JSON . parse ( project . workflow ) ;
73- if ( flow . nodes ) setNodes ( flow . nodes ) ;
73+ if ( flow . nodes )
74+ setNodes (
75+ flow . nodes . map ( ( n : any ) => ( {
76+ ...n ,
77+ data : {
78+ ...n . data ,
79+ processing : false ,
80+ error : undefined ,
81+ runTrigger : undefined ,
82+ } ,
83+ } ) )
84+ ) ;
7485 if ( flow . edges ) setEdges ( flow . edges ) ;
7586
7687 // Update ID counter based on highest ID found
7788 let maxId = 0 ;
7889 if ( Array . isArray ( flow . nodes ) ) {
7990 flow . nodes . forEach ( ( n : Node ) => {
80- const idNum = parseInt ( n . id . replace ( / [ ^ 0 - 9 ] / g, '' ) ) ;
91+ const idNum = parseInt ( n . id . replace ( / [ ^ 0 - 9 ] / g, "" ) ) ;
8192 if ( ! isNaN ( idNum ) && idNum > maxId ) maxId = idNum ;
8293 } ) ;
8394 }
@@ -94,46 +105,34 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
94105 const saveProject = useCallback ( async ( ) => {
95106 if ( ! project . id ) return ;
96107
108+ const nodesToSave = nodes . map ( ( n : any ) => ( {
109+ ...n ,
110+ data : {
111+ ...n . data ,
112+ processing : false ,
113+ error : undefined ,
114+ runTrigger : undefined ,
115+ } ,
116+ } ) ) ;
117+
97118 const workflow = JSON . stringify ( {
98- nodes,
119+ nodes : nodesToSave ,
99120 edges,
100121 } ) ;
101122
102123 try {
103- await SaveProject ( new database . Project ( {
104- ...project ,
105- name : name ,
106- workflow,
107- } ) ) ;
124+ await SaveProject (
125+ new database . Project ( {
126+ ...project ,
127+ name : name ,
128+ workflow,
129+ } )
130+ ) ;
108131 } catch ( err ) {
109132 console . error ( "Auto-save failed:" , err ) ;
110133 }
111134 } , [ nodes , edges , name , project ] ) ;
112135
113- // Auto-save
114- useEffect ( ( ) => {
115- if ( ! isInitialized ) return ;
116-
117- const save = async ( ) => {
118- const workflow = JSON . stringify ( {
119- nodes,
120- edges,
121- } ) ;
122-
123- try {
124- await SaveProject ( new database . Project ( {
125- ...project ,
126- name : name ,
127- workflow,
128- } ) ) ;
129- } catch ( err ) {
130- console . error ( "Auto-save failed:" , err ) ;
131- }
132- } ;
133-
134- save ( ) ;
135- } , [ nodes , edges , name , isInitialized , saveProject ] ) ;
136-
137136 // 拖拽开始 - 暂停自动保存
138137 const onNodeDragStart = useCallback ( ( _ : React . MouseEvent , node : Node ) => {
139138 isDragging . current = true ;
@@ -143,12 +142,13 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
143142 } , [ ] ) ;
144143
145144 // 拖拽结束 - 立即保存
146- const onNodeDragStop = useCallback ( ( _ : React . MouseEvent , node : Node ) => {
147- isDragging . current = false ;
148- saveProject ( ) ;
149- } , [ saveProject ] ) ;
150-
151-
145+ const onNodeDragStop = useCallback (
146+ ( _ : React . MouseEvent , node : Node ) => {
147+ isDragging . current = false ;
148+ saveProject ( ) ;
149+ } ,
150+ [ saveProject ]
151+ ) ;
152152
153153 const [ selectedNodes , setSelectedNodes ] = useState < string [ ] > ( [ ] ) ;
154154
@@ -164,17 +164,22 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
164164 [ ]
165165 ) ;
166166
167- const onSelectionChange = useCallback ( ( { nodes : selectedNodes } : { nodes : Node [ ] } ) => {
168- if ( selectedNodes . some ( ( node ) => node . type === "group" ) ) {
169- return ;
170- }
171- setSelectedNodes ( selectedNodes . map ( ( node ) => node . id ) ) ;
172- } , [ ] ) ;
167+ const onSelectionChange = useCallback (
168+ ( { nodes : selectedNodes } : { nodes : Node [ ] } ) => {
169+ if ( selectedNodes . some ( ( node ) => node . type === "group" ) ) {
170+ return ;
171+ }
172+ setSelectedNodes ( selectedNodes . map ( ( node ) => node . id ) ) ;
173+ } ,
174+ [ ]
175+ ) ;
173176
174177 const createGroup = useCallback ( ( ) => {
175178 if ( selectedNodes . length === 0 ) return ;
176179
177- const selectedNodeObjects = nodes . filter ( ( n ) => selectedNodes . includes ( n . id ) && n . type !== "group" ) ;
180+ const selectedNodeObjects = nodes . filter (
181+ ( n ) => selectedNodes . includes ( n . id ) && n . type !== "group"
182+ ) ;
178183 if ( selectedNodeObjects . length === 0 ) return ;
179184
180185 // Calculate bounding box
@@ -241,14 +246,15 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
241246 y : Math . random ( ) * 400 + 100 ,
242247 } ,
243248 data : {
244- label : `${ type === "text"
245- ? "文本"
246- : type === "image"
249+ label : `${
250+ type === "text"
251+ ? "文本"
252+ : type === "image"
247253 ? "图片"
248254 : type === "video"
249- ? "视频"
250- : "音频"
251- } 节点 ${ nodeIdCounter } `,
255+ ? "视频"
256+ : "音频"
257+ } 节点 ${ nodeIdCounter } `,
252258 type : type ,
253259 } ,
254260 } ;
@@ -259,13 +265,19 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
259265 ) ;
260266
261267 const { getIntersectingNodes } = useReactFlow ( ) ;
262- const dragRef = useRef < { id : string ; position : { x : number ; y : number } } | null > ( null ) ;
263-
268+ const dragRef = useRef < {
269+ id : string ;
270+ position : { x : number ; y : number } ;
271+ } | null > ( null ) ;
264272
265273 // group 子节点跟随拖动
266274 const onNodeDrag = useCallback (
267275 ( _ : React . MouseEvent , node : Node ) => {
268- if ( node . type === "group" && dragRef . current && dragRef . current . id === node . id ) {
276+ if (
277+ node . type === "group" &&
278+ dragRef . current &&
279+ dragRef . current . id === node . id
280+ ) {
269281 const dx = node . position . x - dragRef . current . position . x ;
270282 const dy = node . position . y - dragRef . current . position . y ;
271283
@@ -283,7 +295,9 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
283295 if ( intersectingNodes . length > 0 ) {
284296 setNodes ( ( nds ) =>
285297 nds . map ( ( n : Node ) => {
286- if ( intersectingNodes . some ( ( inNode : Node ) => inNode . id === n . id ) ) {
298+ if (
299+ intersectingNodes . some ( ( inNode : Node ) => inNode . id === n . id )
300+ ) {
287301 return {
288302 ...n ,
289303 position : {
@@ -303,17 +317,22 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
303317 [ getIntersectingNodes , setNodes ]
304318 ) ;
305319
306- const onNodeClick = useCallback ( ( _ : React . MouseEvent , node : Node ) => {
307- if ( node . type === "group" ) {
308- // Manual selection toggle for group nodes since they are not selectable by box
309- setNodes ( ( nds ) => nds . map ( n => {
310- if ( n . id === node . id ) {
311- return { ...n , selected : ! n . selected } ;
312- }
313- return n ;
314- } ) ) ;
315- }
316- } , [ setNodes ] ) ;
320+ const onNodeClick = useCallback (
321+ ( _ : React . MouseEvent , node : Node ) => {
322+ if ( node . type === "group" ) {
323+ // Manual selection toggle for group nodes since they are not selectable by box
324+ setNodes ( ( nds ) =>
325+ nds . map ( ( n ) => {
326+ if ( n . id === node . id ) {
327+ return { ...n , selected : ! n . selected } ;
328+ }
329+ return n ;
330+ } )
331+ ) ;
332+ }
333+ } ,
334+ [ setNodes ]
335+ ) ;
317336
318337 const { getNodes } = useReactFlow ( ) ;
319338
@@ -325,17 +344,26 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
325344 if ( nodesBounds . width > 0 && nodesBounds . height > 0 ) {
326345 const imageWidth = 800 ; // Efficient size for cover
327346 const imageHeight = 450 ;
328- const viewport = getViewportForBounds ( nodesBounds , imageWidth , imageHeight , 0.5 , 2 , 0 ) ;
347+ const viewport = getViewportForBounds (
348+ nodesBounds ,
349+ imageWidth ,
350+ imageHeight ,
351+ 0.5 ,
352+ 2 ,
353+ 0
354+ ) ;
329355
330- const viewportElement = document . querySelector ( '.react-flow__viewport' ) as HTMLElement ;
356+ const viewportElement = document . querySelector (
357+ ".react-flow__viewport"
358+ ) as HTMLElement ;
331359 if ( viewportElement ) {
332360 const dataUrl = await toPng ( viewportElement , {
333- backgroundColor : ' transparent' ,
361+ backgroundColor : " transparent" ,
334362 width : imageWidth ,
335363 height : imageHeight ,
336364 filter : ( node ) => {
337365 // Exclude video elements to prevent SecurityError: The operation is insecure.
338- if ( node . tagName === ' VIDEO' ) return false ;
366+ if ( node . tagName === " VIDEO" ) return false ;
339367 return true ;
340368 } ,
341369 style : {
@@ -354,20 +382,7 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
354382 }
355383
356384 // 3. Save Project (explicit save to ensure cover image is persisted)
357- try {
358- const workflow = JSON . stringify ( {
359- nodes,
360- edges,
361- } ) ;
362- await SaveProject ( new database . Project ( {
363- ...project ,
364- name : name ,
365- workflow,
366- coverImage : project . coverImage // Ensure this is sent
367- } ) ) ;
368- } catch ( err ) {
369- console . error ( "Failed to save project on exit:" , err ) ;
370- }
385+ saveProject ( ) ;
371386
372387 // 4. Navigate back
373388 onBack ( ) ;
@@ -379,7 +394,8 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
379394 edges,
380395 } ;
381396 const jsonString = JSON . stringify ( data , null , 2 ) ;
382- navigator . clipboard . writeText ( jsonString )
397+ navigator . clipboard
398+ . writeText ( jsonString )
383399 . then ( ( ) => toast . success ( "Project JSON copied to clipboard" ) )
384400 . catch ( ( err ) => {
385401 console . error ( "Failed to copy:" , err ) ;
@@ -393,37 +409,40 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
393409 fileInputRef . current ?. click ( ) ;
394410 } ;
395411
396- const handleFileChange = useCallback ( ( event : React . ChangeEvent < HTMLInputElement > ) => {
397- const file = event . target . files ?. [ 0 ] ;
398- if ( ! file ) return ;
399-
400- const reader = new FileReader ( ) ;
401- reader . onload = ( e ) => {
402- try {
403- const content = e . target ?. result as string ;
404- const data = JSON . parse ( content ) ;
405- if ( data . nodes && data . edges ) {
406- setNodes ( data . nodes ) ;
407- setEdges ( data . edges ) ;
408- // Update ID counter based on highest ID found to avoid collisions
409- let maxId = 0 ;
410- data . nodes . forEach ( ( n : Node ) => {
411- const idNum = parseInt ( n . id . replace ( / [ ^ 0 - 9 ] / g, '' ) ) ;
412- if ( ! isNaN ( idNum ) && idNum > maxId ) maxId = idNum ;
413- } ) ;
414- setNodeIdCounter ( maxId + 1 ) ;
415- } else {
416- alert ( "Invalid project file format" ) ;
412+ const handleFileChange = useCallback (
413+ ( event : React . ChangeEvent < HTMLInputElement > ) => {
414+ const file = event . target . files ?. [ 0 ] ;
415+ if ( ! file ) return ;
416+
417+ const reader = new FileReader ( ) ;
418+ reader . onload = ( e ) => {
419+ try {
420+ const content = e . target ?. result as string ;
421+ const data = JSON . parse ( content ) ;
422+ if ( data . nodes && data . edges ) {
423+ setNodes ( data . nodes ) ;
424+ setEdges ( data . edges ) ;
425+ // Update ID counter based on highest ID found to avoid collisions
426+ let maxId = 0 ;
427+ data . nodes . forEach ( ( n : Node ) => {
428+ const idNum = parseInt ( n . id . replace ( / [ ^ 0 - 9 ] / g, "" ) ) ;
429+ if ( ! isNaN ( idNum ) && idNum > maxId ) maxId = idNum ;
430+ } ) ;
431+ setNodeIdCounter ( maxId + 1 ) ;
432+ } else {
433+ alert ( "Invalid project file format" ) ;
434+ }
435+ } catch ( err ) {
436+ console . error ( "Import failed:" , err ) ;
437+ alert ( "Failed to parse project file" ) ;
417438 }
418- } catch ( err ) {
419- console . error ( "Import failed:" , err ) ;
420- alert ( "Failed to parse project file" ) ;
421- }
422- } ;
423- reader . readAsText ( file ) ;
424- // Reset input so same file can be selected again
425- event . target . value = "" ;
426- } , [ setNodes , setEdges , setNodeIdCounter ] ) ;
439+ } ;
440+ reader . readAsText ( file ) ;
441+ // Reset input so same file can be selected again
442+ event . target . value = "" ;
443+ } ,
444+ [ setNodes , setEdges , setNodeIdCounter ]
445+ ) ;
427446
428447 return (
429448 < div className = "flex h-full relative" >
@@ -479,7 +498,7 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
479498
480499 { /* 左侧悬浮工具栏 */ }
481500 < div className = "absolute left-4 top-1/2 -translate-y-1/2 z-10 flex flex-col gap-2" >
482- < Card className = "p-2 shadow-lg" >
501+ < Card className = "p-2 shadow-lg rounded-full " >
483502 < div className = "flex flex-col gap-2" >
484503 < Button
485504 variant = "ghost"
0 commit comments