This commit is contained in:
dengqichen 2025-10-21 10:38:17 +08:00
parent 051709ed2a
commit ea5ca6601a
2 changed files with 273 additions and 11 deletions

View File

@ -0,0 +1,117 @@
import { useState, useCallback, useRef } from 'react';
import type { FlowNode, FlowEdge } from '../types';
/**
*
*/
interface HistoryState {
nodes: FlowNode[];
edges: FlowEdge[];
}
/**
* Hook
* /
*/
export const useHistory = () => {
const [history, setHistory] = useState<HistoryState[]>([]);
const [currentIndex, setCurrentIndex] = useState(-1);
const isRecording = useRef(true);
/**
*
*/
const record = useCallback((nodes: FlowNode[], edges: FlowEdge[]) => {
if (!isRecording.current) return;
setHistory(prev => {
// 移除当前索引之后的所有历史
const newHistory = prev.slice(0, currentIndex + 1);
// 添加新状态
newHistory.push({
nodes: JSON.parse(JSON.stringify(nodes)),
edges: JSON.parse(JSON.stringify(edges))
});
// 限制历史记录数量最多50条
if (newHistory.length > 50) {
newHistory.shift();
setCurrentIndex(prev => prev);
} else {
setCurrentIndex(newHistory.length - 1);
}
return newHistory;
});
}, [currentIndex]);
/**
*
*/
const undo = useCallback((): HistoryState | null => {
if (currentIndex <= 0) {
return null;
}
const newIndex = currentIndex - 1;
setCurrentIndex(newIndex);
return history[newIndex];
}, [currentIndex, history]);
/**
*
*/
const redo = useCallback((): HistoryState | null => {
if (currentIndex >= history.length - 1) {
return null;
}
const newIndex = currentIndex + 1;
setCurrentIndex(newIndex);
return history[newIndex];
}, [currentIndex, history]);
/**
*
*/
const canUndo = useCallback(() => {
return currentIndex > 0;
}, [currentIndex]);
/**
*
*/
const canRedo = useCallback(() => {
return currentIndex < history.length - 1;
}, [currentIndex, history.length]);
/**
*
*/
const pauseRecording = useCallback(() => {
isRecording.current = false;
}, []);
/**
*
*/
const resumeRecording = useCallback(() => {
isRecording.current = true;
}, []);
/**
*
*/
const clear = useCallback(() => {
setHistory([]);
setCurrentIndex(-1);
}, []);
return {
record,
undo,
redo,
canUndo: canUndo(),
canRedo: canRedo(),
pauseRecording,
resumeRecording,
clear
};
};

View File

@ -13,6 +13,7 @@ import type { WorkflowNodeDefinition } from './nodes/types';
import { NodeType } from './types'; import { NodeType } from './types';
import { useWorkflowSave } from './hooks/useWorkflowSave'; import { useWorkflowSave } from './hooks/useWorkflowSave';
import { useWorkflowLoad } from './hooks/useWorkflowLoad'; import { useWorkflowLoad } from './hooks/useWorkflowLoad';
import { useHistory } from './hooks/useHistory';
// 样式 // 样式
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
@ -51,6 +52,12 @@ const WorkflowDesignInner: React.FC = () => {
// 保存和加载hooks // 保存和加载hooks
const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave(); const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { workflowDefinition, loadWorkflow } = useWorkflowLoad(); const { workflowDefinition, loadWorkflow } = useWorkflowLoad();
// 历史记录管理
const history = useHistory();
// 剪贴板(用于复制粘贴)
const clipboard = useRef<{ nodes: FlowNode[]; edges: FlowEdge[] } | null>(null);
// 加载工作流数据 // 加载工作流数据
useEffect(() => { useEffect(() => {
@ -184,25 +191,109 @@ const WorkflowDesignInner: React.FC = () => {
navigate('/workflow2/definition'); navigate('/workflow2/definition');
}, [navigate]); }, [navigate]);
// 撤销操作
const handleUndo = useCallback(() => { const handleUndo = useCallback(() => {
console.log('撤销操作'); const state = history.undo();
message.info('撤销功能开发中...'); if (state) {
}, []); history.pauseRecording();
setNodes(state.nodes);
setEdges(state.edges);
setTimeout(() => history.resumeRecording(), 100);
message.success('已撤销');
} else {
message.info('没有可撤销的操作');
}
}, [history, setNodes, setEdges]);
// 重做操作
const handleRedo = useCallback(() => { const handleRedo = useCallback(() => {
console.log('重做操作'); const state = history.redo();
message.info('重做功能开发中...'); if (state) {
}, []); history.pauseRecording();
setNodes(state.nodes);
setEdges(state.edges);
setTimeout(() => history.resumeRecording(), 100);
message.success('已重做');
} else {
message.info('没有可重做的操作');
}
}, [history, setNodes, setEdges]);
// 复制选中的节点和边
const handleCopy = useCallback(() => { const handleCopy = useCallback(() => {
const selectedNodes = getNodes().filter(node => node.selected); const selectedNodes = getNodes().filter(node => node.selected);
const selectedEdges = getEdges().filter(edge => {
// 只复制两端都被选中的边
return selectedNodes.some(n => n.id === edge.source) &&
selectedNodes.some(n => n.id === edge.target);
});
if (selectedNodes.length > 0) { if (selectedNodes.length > 0) {
console.log('复制节点:', selectedNodes); clipboard.current = {
nodes: JSON.parse(JSON.stringify(selectedNodes)),
edges: JSON.parse(JSON.stringify(selectedEdges))
};
message.success(`已复制 ${selectedNodes.length} 个节点`); message.success(`已复制 ${selectedNodes.length} 个节点`);
} else { } else {
message.warning('请先选择要复制的节点'); message.warning('请先选择要复制的节点');
} }
}, [getNodes]); }, [getNodes, getEdges]);
// 粘贴节点
const handlePaste = useCallback(() => {
if (!clipboard.current || clipboard.current.nodes.length === 0) {
message.info('剪贴板为空');
return;
}
const { nodes: copiedNodes, edges: copiedEdges } = clipboard.current;
const offset = 50; // 粘贴偏移量
const idMap = new Map<string, string>();
// 创建新节点(带偏移)
const newNodes = copiedNodes.map(node => {
const newId = `${node.type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
idMap.set(node.id, newId);
return {
...node,
id: newId,
position: {
x: node.position.x + offset,
y: node.position.y + offset
},
selected: true
};
});
// 创建新边更新source和target为新ID
const newEdges = copiedEdges.map(edge => {
const newSource = idMap.get(edge.source);
const newTarget = idMap.get(edge.target);
if (!newSource || !newTarget) return null;
return {
...edge,
id: `e${newSource}-${newTarget}`,
source: newSource,
target: newTarget,
selected: true
};
}).filter(edge => edge !== null) as FlowEdge[];
// 取消其他元素的选中状态
setNodes(nodes => [
...nodes.map(n => ({ ...n, selected: false })),
...newNodes
]);
setEdges(edges => [
...edges.map(e => ({ ...e, selected: false })),
...newEdges
]);
message.success(`已粘贴 ${newNodes.length} 个节点`);
}, [setNodes, setEdges]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
const selectedNodes = getNodes().filter(node => node.selected); const selectedNodes = getNodes().filter(node => node.selected);
@ -212,10 +303,11 @@ const WorkflowDesignInner: React.FC = () => {
setNodes(nodes => nodes.filter(node => !node.selected)); setNodes(nodes => nodes.filter(node => !node.selected));
setEdges(edges => edges.filter(edge => !edge.selected)); setEdges(edges => edges.filter(edge => !edge.selected));
message.success(`已删除 ${selectedNodes.length} 个节点和 ${selectedEdges.length} 条连接`); message.success(`已删除 ${selectedNodes.length} 个节点和 ${selectedEdges.length} 条连接`);
markUnsaved();
} else { } else {
message.warning('请先选择要删除的元素'); message.warning('请先选择要删除的元素');
} }
}, [getNodes, getEdges, setNodes, setEdges]); }, [getNodes, getEdges, setNodes, setEdges, markUnsaved]);
const handleSelectAll = useCallback(() => { const handleSelectAll = useCallback(() => {
setNodes(nodes => nodes.map(node => ({ ...node, selected: true }))); setNodes(nodes => nodes.map(node => ({ ...node, selected: true })));
@ -372,6 +464,59 @@ const WorkflowDesignInner: React.FC = () => {
setCurrentZoom(zoom); setCurrentZoom(zoom);
}, [getZoom]); }, [getZoom]);
// 监听节点和边的变化,记录到历史
useEffect(() => {
const nodes = getNodes() as FlowNode[];
const edges = getEdges() as FlowEdge[];
// 只在有节点或边时记录(避免空状态)
if (nodes.length > 0 || edges.length > 0) {
history.record(nodes, edges);
}
}, [getNodes, getEdges, history]);
// 键盘快捷键支持
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const ctrlKey = isMac ? e.metaKey : e.ctrlKey;
// Ctrl+Z / Cmd+Z - 撤销
if (ctrlKey && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
handleUndo();
}
// Ctrl+Shift+Z / Cmd+Shift+Z - 重做
else if (ctrlKey && e.key === 'z' && e.shiftKey) {
e.preventDefault();
handleRedo();
}
// Ctrl+C / Cmd+C - 复制
else if (ctrlKey && e.key === 'c') {
e.preventDefault();
handleCopy();
}
// Ctrl+V / Cmd+V - 粘贴
else if (ctrlKey && e.key === 'v') {
e.preventDefault();
handlePaste();
}
// Ctrl+A / Cmd+A - 全选
else if (ctrlKey && e.key === 'a') {
e.preventDefault();
handleSelectAll();
}
// Delete / Backspace - 删除
else if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
handleDelete();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete]);
return ( return (
<div <div
className="workflow-design-container" className="workflow-design-container"
@ -393,8 +538,8 @@ const WorkflowDesignInner: React.FC = () => {
onZoomIn={handleZoomIn} onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut} onZoomOut={handleZoomOut}
onFitView={handleFitView} onFitView={handleFitView}
canUndo={false} canUndo={history.canUndo}
canRedo={false} canRedo={history.canRedo}
zoom={currentZoom} zoom={currentZoom}
/> />