You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

177 lines
5.0 KiB
TypeScript

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;
}
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>
);
};
export default HistoryProvider;