diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index dfc42973..b229c01e 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2515,6 +2515,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re arrange: 'Arrange', redo: 'Redo', undo: 'Undo', + fit: 'Fit View', input: 'Input', output: 'Output', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index ae0181c9..dae693e2 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2479,6 +2479,7 @@ export const zh = { arrange: '整理', redo: '重做', undo: '撤销', + fit: '自适应', input: '输入', output: '输出', diff --git a/web/src/views/Workflow/components/CanvasToolbar.tsx b/web/src/views/Workflow/components/CanvasToolbar.tsx index 6a2cbc7f..1bbb51f2 100644 --- a/web/src/views/Workflow/components/CanvasToolbar.tsx +++ b/web/src/views/Workflow/components/CanvasToolbar.tsx @@ -1,8 +1,9 @@ import type { FC } from 'react'; -import { Select, Divider } from 'antd'; -import { PlusOutlined, MinusOutlined, FileAddOutlined } from '@ant-design/icons' +import { Select, Divider, Tooltip } from 'antd'; +import { PlusOutlined, MinusOutlined, FileAddOutlined, UndoOutlined, RedoOutlined } from '@ant-design/icons' import clsx from 'clsx' import { Node } from '@antv/x6'; +import { useTranslation } from 'react-i18next' import type { GraphRef } from '../types' @@ -15,6 +16,10 @@ interface CanvasToolbarProps { setIsHandMode: React.Dispatch>; zoomLevel: number; addNotes: () => void; + canUndo: boolean; + canRedo: boolean; + onUndo: () => void; + onRedo: () => void; } const CanvasToolbar: FC = ({ @@ -22,12 +27,13 @@ const CanvasToolbar: FC = ({ miniMapRef, graphRef, zoomLevel, - // canUndo, - // canRedo, - // onUndo, - // onRedo, + canUndo, + canRedo, + onUndo, + onRedo, addNotes, }) => { + const { t } = useTranslation() return ( <> {/* 小地图 */} @@ -63,13 +69,16 @@ const CanvasToolbar: FC = ({ { label: '125%', value: 125 }, { label: '150%', value: 150 }, { label: '200%', value: 200 }, - { label: '自适应', value: 'fit' }, + { label: t('workflow.fit'), value: 'fit' }, ]} variant='borderless' size="small" /> graphRef.current?.zoom(0.1)} /> + + + diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 9f05cd27..e82ad580 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -2,9 +2,10 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:17:48 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-15 16:02:49 + * @Last Modified time: 2026-04-20 16:00:26 */ -import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, type Edge } from '@antv/x6'; +import { Clipboard, Graph, Keyboard, MiniMap, Node, Snapline, History, type Edge } from '@antv/x6'; +import type { HistoryCommand as Command } from '@antv/x6/lib/plugin/history/type'; import { register } from '@antv/x6-react-shape'; import type { PortMetadata } from '@antv/x6/lib/model/port'; import { App } from 'antd'; @@ -64,6 +65,14 @@ export interface UseWorkflowGraphReturn { copyEvent: () => boolean | void; /** Handler for paste keyboard event */ parseEvent: () => boolean | void; + /** Whether undo is available */ + canUndo: boolean; + /** Whether redo is available */ + canRedo: boolean; + /** Undo last action */ + undo: () => void; + /** Redo last undone action */ + redo: () => void; /** Function to save workflow configuration */ handleSave: (flag?: boolean) => Promise; /** Chat variables for workflow */ @@ -108,6 +117,8 @@ export const useWorkflowGraph = ({ const [config, setConfig] = useState(null); const [chatVariables, setChatVariables] = useState([]) const featuresRef = useRef(undefined) + const [canUndo, setCanUndo] = useState(false) + const [canRedo, setCanRedo] = useState(false) useEffect(() => { if (!graphRef.current) return @@ -473,6 +484,8 @@ export const useWorkflowGraph = ({ graphRef.current.getNodes().forEach(node => { if (node.getData()?.cycle) node.toFront(); }); + graphRef.current.enableHistory() + graphRef.current.cleanHistory() } }, 200) } @@ -508,6 +521,22 @@ export const useWorkflowGraph = ({ global: true, }), ); + graphRef.current.use( + new History({ + enabled: false, + beforeAddCommand(_event, args: any) { + const event = args?.key ? `cell:change:${args.key}` : _event; + if (event.startsWith('cell:change:') && + event !== 'cell:change:position' && + event !== 'cell:change:source' && + event !== 'cell:change:target') return false; + }, + }), + ); + graphRef.current.on('history:change', ({ cmds }: { cmds: Command[] }) => { + setCanUndo(graphRef.current?.canUndo() ?? false) + setCanRedo(graphRef.current?.canRedo() ?? false) + }) }; // 显示/隐藏连接桩 // const showPorts = (show: boolean) => { @@ -1096,6 +1125,9 @@ export const useWorkflowGraph = ({ graphRef.current.bindKey(['ctrl+v', 'cmd+v'], parseEvent); // Delete selected nodes and edges graphRef.current.bindKey(['ctrl+d', 'cmd+d', 'delete', 'backspace'], deleteEvent); + // Undo / Redo + graphRef.current.bindKey(['ctrl+z', 'cmd+z'], () => { graphRef.current?.undo(); return false; }); + graphRef.current.bindKey(['ctrl+y', 'cmd+y', 'ctrl+shift+z', 'cmd+shift+z'], () => { graphRef.current?.redo(); return false; }); }; @@ -1414,6 +1446,9 @@ export const useWorkflowGraph = ({ return userVars } + const undo = () => graphRef.current?.undo() + const redo = () => graphRef.current?.redo() + const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => { const { statement = '' } = value?.opening_statement || {} featuresRef.current = value @@ -1498,5 +1533,9 @@ export const useWorkflowGraph = ({ handleSaveFeaturesConfig, features: featuresRef.current, getStartNodeVariables, + canUndo, + canRedo, + undo, + redo, }; }; diff --git a/web/src/views/Workflow/index.tsx b/web/src/views/Workflow/index.tsx index 26d7420c..f98cf308 100644 --- a/web/src/views/Workflow/index.tsx +++ b/web/src/views/Workflow/index.tsx @@ -39,6 +39,10 @@ const Workflow = forwardRef { @@ -96,6 +100,10 @@ const Workflow = forwardRef