feat(flowEditor): 添加历史记录功能和撤销重做操作
- 在 actionBar 中添加撤销和重做按钮 - 实现 HistoryProvider 和 useHistory hook 来管理历史记录 - 为节点和边的变化添加快照记录机制 - 支持通过快捷键 Ctrl+Z 撤销和 Ctrl+Y/Ctrl+Shift+Z 重做- 在节点拖动、连接创建、节点删除等操作后自动记录历史- 添加防抖机制避免频繁的位置变化记录 - 实现历史记录长度限制防止内存泄漏 - 更新 FlowEditor 组件结构以支持历史记录功能master
parent
3c57f650fb
commit
1a8629d84c
@ -0,0 +1,172 @@
|
|||||||
|
import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { Node, Edge } from '@xyflow/react';
|
||||||
|
|
||||||
|
interface HistoryContextType {
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
canUndo: boolean;
|
||||||
|
canRedo: boolean;
|
||||||
|
takeSnapshot: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoryContext = createContext<HistoryContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useHistory = () => {
|
||||||
|
const context = useContext(HistoryContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useHistory must be used within a HistoryProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface HistoryProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
initialNodes: Node[];
|
||||||
|
initialEdges: Edge[];
|
||||||
|
onHistoryChange: (nodes: Node[], edges: Edge[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HistoryProvider: React.FC<HistoryProviderProps> = ({
|
||||||
|
children,
|
||||||
|
initialNodes,
|
||||||
|
initialEdges,
|
||||||
|
onHistoryChange
|
||||||
|
}) => {
|
||||||
|
// 历史记录状态
|
||||||
|
const [history, setHistory] = useState<{ nodes: Node[]; edges: Edge[] }[]>([
|
||||||
|
{ nodes: initialNodes, edges: initialEdges }
|
||||||
|
]);
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
|
||||||
|
// 当前状态的引用,避免重复添加相同状态
|
||||||
|
const currentState = useRef({
|
||||||
|
nodes: initialNodes,
|
||||||
|
edges: initialEdges
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查两个状态是否相等
|
||||||
|
const isSameState = useCallback((state1: { nodes: Node[]; edges: Edge[] }, state2: { nodes: Node[]; edges: Edge[] }) => {
|
||||||
|
// 只比较节点和边的关键属性,忽略拖动过程中的临时状态
|
||||||
|
if (state1.nodes.length !== state2.nodes.length || state1.edges.length !== state2.edges.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较节点
|
||||||
|
for (let i = 0; i < state1.nodes.length; i++) {
|
||||||
|
const node1 = state1.nodes[i];
|
||||||
|
const node2 = state2.nodes[i];
|
||||||
|
|
||||||
|
if (node1.id !== node2.id ||
|
||||||
|
node1.type !== node2.type ||
|
||||||
|
node1.position.x !== node2.position.x ||
|
||||||
|
node1.position.y !== node2.position.y ||
|
||||||
|
JSON.stringify(node1.data) !== JSON.stringify(node2.data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较边
|
||||||
|
for (let i = 0; i < state1.edges.length; i++) {
|
||||||
|
const edge1 = state1.edges[i];
|
||||||
|
const edge2 = state2.edges[i];
|
||||||
|
|
||||||
|
if (edge1.id !== edge2.id ||
|
||||||
|
edge1.source !== edge2.source ||
|
||||||
|
edge1.target !== edge2.target ||
|
||||||
|
edge1.sourceHandle !== edge2.sourceHandle ||
|
||||||
|
edge1.targetHandle !== edge2.targetHandle) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 拍摄快照
|
||||||
|
const takeSnapshot = useCallback(() => {
|
||||||
|
// 获取当前状态
|
||||||
|
const { nodes, edges } = currentState.current;
|
||||||
|
|
||||||
|
// 如果当前状态与历史记录中的当前步骤相同,则不添加新快照
|
||||||
|
const currentHistoryState = history[step];
|
||||||
|
if (isSameState({ nodes, edges }, currentHistoryState)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除当前步骤之后的所有历史记录
|
||||||
|
const newHistory = history.slice(0, step + 1);
|
||||||
|
|
||||||
|
// 添加新快照
|
||||||
|
newHistory.push({
|
||||||
|
nodes: nodes.map(node => ({...node})),
|
||||||
|
edges: edges.map(edge => ({...edge}))
|
||||||
|
});
|
||||||
|
|
||||||
|
// 限制历史记录长度,防止内存泄漏
|
||||||
|
const maxLength = 100;
|
||||||
|
if (newHistory.length > maxLength) {
|
||||||
|
newHistory.shift();
|
||||||
|
setStep(prev => prev - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistory(newHistory);
|
||||||
|
setStep(newHistory.length - 1);
|
||||||
|
}, [history, step, isSameState]);
|
||||||
|
|
||||||
|
// 撤销操作
|
||||||
|
const undo = useCallback(() => {
|
||||||
|
if (step <= 0) return;
|
||||||
|
|
||||||
|
const prevStep = step - 1;
|
||||||
|
const { nodes, edges } = history[prevStep];
|
||||||
|
|
||||||
|
currentState.current = { nodes, edges };
|
||||||
|
setStep(prevStep);
|
||||||
|
onHistoryChange([...nodes], [...edges]);
|
||||||
|
}, [step, history, onHistoryChange]);
|
||||||
|
|
||||||
|
// 重做操作
|
||||||
|
const redo = useCallback(() => {
|
||||||
|
if (step >= history.length - 1) return;
|
||||||
|
|
||||||
|
const nextStep = step + 1;
|
||||||
|
const { nodes, edges } = history[nextStep];
|
||||||
|
|
||||||
|
currentState.current = { nodes, edges };
|
||||||
|
setStep(nextStep);
|
||||||
|
onHistoryChange([...nodes], [...edges]);
|
||||||
|
}, [step, history, onHistoryChange]);
|
||||||
|
|
||||||
|
// 更新当前状态的引用
|
||||||
|
const updateCurrentState = useCallback((nodes: Node[], edges: Edge[]) => {
|
||||||
|
currentState.current = { nodes, edges };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 监听 takeSnapshot 事件
|
||||||
|
useEffect(() => {
|
||||||
|
const handleTakeSnapshot = ((event: CustomEvent) => {
|
||||||
|
const { nodes, edges } = event.detail;
|
||||||
|
updateCurrentState(nodes, edges);
|
||||||
|
takeSnapshot();
|
||||||
|
}) as EventListener;
|
||||||
|
|
||||||
|
document.addEventListener('takeSnapshot', handleTakeSnapshot);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('takeSnapshot', handleTakeSnapshot);
|
||||||
|
};
|
||||||
|
}, [takeSnapshot, updateCurrentState]);
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
canUndo: step > 0,
|
||||||
|
canRedo: step < history.length - 1,
|
||||||
|
takeSnapshot
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HistoryContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</HistoryContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue