From fcdf005e22c0c6e8ac76f0948bb2cf89b9b0b574 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Mon, 10 Nov 2025 13:56:26 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=89=8D=E7=AB=AF=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/Design/components/FlowCanvas.tsx | 30 ++-- .../components/KeyboardShortcutsPanel.tsx | 103 +++++++++++++ .../Design/components/NodePanel.module.css | 106 +++++++++++++ .../Workflow/Design/components/NodePanel.tsx | 116 +++------------ frontend/src/pages/Workflow/Design/index.tsx | 139 +++++++++++++----- 5 files changed, 351 insertions(+), 143 deletions(-) create mode 100644 frontend/src/pages/Workflow/Design/components/KeyboardShortcutsPanel.tsx create mode 100644 frontend/src/pages/Workflow/Design/components/NodePanel.module.css diff --git a/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx b/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx index 45dda854..09006542 100644 --- a/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx +++ b/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { ReactFlow, Background, @@ -80,24 +80,26 @@ const FlowCanvas: React.FC = ({ // 节点变化处理 const handleNodesChange = useCallback((changes: any) => { onNodesStateChange(changes); - if (onNodesChange) { - // 延迟获取最新状态 - 在实际项目中可以使用useEffect监听nodes变化 - setTimeout(() => { - onNodesChange(nodes as FlowNode[]); - }, 0); - } - }, [onNodesStateChange, onNodesChange, nodes]); + }, [onNodesStateChange]); // 边变化处理 const handleEdgesChange = useCallback((changes: any) => { onEdgesStateChange(changes); - if (onEdgesChange) { - // 延迟获取最新状态 - 在实际项目中可以使用useEffect监听edges变化 - setTimeout(() => { - onEdgesChange(edges as FlowEdge[]); - }, 0); + }, [onEdgesStateChange]); + + // 使用 useEffect 监听 nodes 变化并通知父组件 + useEffect(() => { + if (onNodesChange && nodes.length > 0) { + onNodesChange(nodes as FlowNode[]); } - }, [onEdgesStateChange, onEdgesChange, edges]); + }, [nodes, onNodesChange]); + + // 使用 useEffect 监听 edges 变化并通知父组件 + useEffect(() => { + if (onEdgesChange) { + onEdgesChange(edges as FlowEdge[]); + } + }, [edges, onEdgesChange]); // 连接验证 - 利用 React Flow 的实时验证功能 const isValidConnection = useCallback((connection: Connection | Edge) => { diff --git a/frontend/src/pages/Workflow/Design/components/KeyboardShortcutsPanel.tsx b/frontend/src/pages/Workflow/Design/components/KeyboardShortcutsPanel.tsx new file mode 100644 index 00000000..b5e6f1eb --- /dev/null +++ b/frontend/src/pages/Workflow/Design/components/KeyboardShortcutsPanel.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody } from '@/components/ui/dialog'; +import { Keyboard } from 'lucide-react'; + +interface KeyboardShortcutsPanelProps { + open: boolean; + onClose: () => void; +} + +interface Shortcut { + keys: string[]; + description: string; + category: string; +} + +const shortcuts: Shortcut[] = [ + // 基本操作 + { keys: ['Ctrl', 'S'], description: '保存工作流', category: '基本操作' }, + { keys: ['Ctrl', 'Z'], description: '撤销', category: '基本操作' }, + { keys: ['Ctrl', 'Y'], description: '重做', category: '基本操作' }, + { keys: ['Ctrl', 'Shift', 'Z'], description: '重做', category: '基本操作' }, + { keys: ['?'], description: '显示快捷键', category: '基本操作' }, + + // 编辑操作 + { keys: ['Ctrl', 'C'], description: '复制选中节点', category: '编辑操作' }, + { keys: ['Ctrl', 'V'], description: '粘贴节点', category: '编辑操作' }, + { keys: ['Ctrl', 'A'], description: '全选节点', category: '编辑操作' }, + { keys: ['Delete'], description: '删除选中节点', category: '编辑操作' }, + { keys: ['Backspace'], description: '删除选中节点', category: '编辑操作' }, + + // 视图操作 + { keys: ['Ctrl', '+'], description: '放大画布', category: '视图操作' }, + { keys: ['Ctrl', '-'], description: '缩小画布', category: '视图操作' }, + { keys: ['Ctrl', '0'], description: '适应视图', category: '视图操作' }, + { keys: ['Space', '+', '拖动'], description: '平移画布', category: '视图操作' }, + + // 节点操作 + { keys: ['双击节点'], description: '配置节点', category: '节点操作' }, + { keys: ['双击边'], description: '配置连接条件', category: '节点操作' }, + { keys: ['Ctrl', '+', '点击'], description: '多选节点', category: '节点操作' }, +]; + +const KeyboardShortcutsPanel: React.FC = ({ open, onClose }) => { + // 按分类分组快捷键 + const groupedShortcuts = shortcuts.reduce((acc, shortcut) => { + if (!acc[shortcut.category]) { + acc[shortcut.category] = []; + } + acc[shortcut.category].push(shortcut); + return acc; + }, {} as Record); + + return ( + + + + + + 键盘快捷键 + + + + +
+ {Object.entries(groupedShortcuts).map(([category, items]) => ( +
+

+ {category} +

+
+ {items.map((shortcut, index) => ( +
+ {shortcut.description} +
+ {shortcut.keys.map((key, keyIndex) => ( + + {keyIndex > 0 && ( + + + )} + + {key} + + + ))} +
+
+ ))} +
+
+ ))} +
+ +
+ 提示:按 ? 键可随时打开此面板 +
+
+
+
+ ); +}; + +export default KeyboardShortcutsPanel; + diff --git a/frontend/src/pages/Workflow/Design/components/NodePanel.module.css b/frontend/src/pages/Workflow/Design/components/NodePanel.module.css new file mode 100644 index 00000000..444b12da --- /dev/null +++ b/frontend/src/pages/Workflow/Design/components/NodePanel.module.css @@ -0,0 +1,106 @@ +.nodePanel { + width: 260px; + height: 100%; + background: #f8fafc; + border-right: 1px solid #e5e7eb; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.header { + padding: 16px; + background: white; + border-bottom: 1px solid #e5e7eb; +} + +.title { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #374151; +} + +.subtitle { + margin: 4px 0 0 0; + font-size: 12px; + color: #6b7280; +} + +.content { + flex: 1; + overflow: auto; + padding: 12px; +} + +.category { + margin-bottom: 16px; +} + +.categoryTitle { + font-size: 12px; + font-weight: 600; + color: #4b5563; + margin-bottom: 8px; + padding: 4px 0; + border-bottom: 1px solid #e5e7eb; +} + +.nodeItem { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 6px; + border: 1px solid #e5e7eb; + background: white; + cursor: grab; + transition: all 0.2s ease-in-out; + margin-bottom: 6px; +} + +.nodeItem:hover { + background: #f9fafb; + transform: translateX(2px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.nodeItem:active { + cursor: grabbing; +} + +.nodeIcon { + font-size: 16px; + width: 20px; + text-align: center; + flex-shrink: 0; +} + +.nodeInfo { + flex: 1; + min-width: 0; +} + +.nodeName { + font-size: 13px; + font-weight: 500; + color: #374151; + line-height: 1.2; +} + +.nodeDesc { + font-size: 11px; + color: #6b7280; + line-height: 1.2; + margin-top: 2px; +} + +.tips { + padding: 12px; + background: #f1f5f9; + border-top: 1px solid #e5e7eb; + font-size: 11px; + color: #64748b; + line-height: 1.4; +} + diff --git a/frontend/src/pages/Workflow/Design/components/NodePanel.tsx b/frontend/src/pages/Workflow/Design/components/NodePanel.tsx index 4f516c21..b48f26e8 100644 --- a/frontend/src/pages/Workflow/Design/components/NodePanel.tsx +++ b/frontend/src/pages/Workflow/Design/components/NodePanel.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { NODE_DEFINITIONS } from '../nodes'; import { NodeCategory } from '../nodes/types'; import type { WorkflowNodeDefinition } from '../nodes/types'; +import styles from './NodePanel.module.css'; interface NodePanelProps { className?: string; @@ -31,59 +32,26 @@ const NodePanel: React.FC = ({ className = '' }) => {
{ - e.currentTarget.style.background = '#f9fafb'; e.currentTarget.style.borderColor = nodeDefinition.renderConfig.theme.primary; - e.currentTarget.style.transform = 'translateX(2px)'; }} onMouseLeave={(e) => { - e.currentTarget.style.background = 'white'; e.currentTarget.style.borderColor = '#e5e7eb'; - e.currentTarget.style.transform = 'translateX(0)'; - }} - onDragStart={(e) => { - e.currentTarget.style.cursor = 'grabbing'; - handleDragStart(e, nodeDefinition); - }} - onDragEnd={(e) => { - e.currentTarget.style.cursor = 'grab'; }} + onDragStart={(e) => handleDragStart(e, nodeDefinition)} > -
+
{nodeDefinition.renderConfig.icon.content}
-
-
+
+
{nodeDefinition.nodeName}
-
+
{nodeDefinition.description}
@@ -99,72 +67,30 @@ const NodePanel: React.FC = ({ className = '' }) => { }; return ( -
-
-

- 节点面板 -

-

- 拖拽节点到画布创建工作流 -

+
+
+

节点面板

+

拖拽节点到画布创建工作流

- -
+ +
{Object.entries(nodesByCategory).map(([category, nodes]) => ( -
-
+
+
{categoryTitles[category as NodeCategory]}
{nodes.map(renderNodeItem)}
))}
- + {/* 使用提示 */} -
+
💡 提示:
• 拖拽节点到画布创建
• 双击节点进行配置
• 连接节点创建流程 +
• 按 ? 查看快捷键
); diff --git a/frontend/src/pages/Workflow/Design/index.tsx b/frontend/src/pages/Workflow/Design/index.tsx index d2fd81ad..db650c3d 100644 --- a/frontend/src/pages/Workflow/Design/index.tsx +++ b/frontend/src/pages/Workflow/Design/index.tsx @@ -2,6 +2,7 @@ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react' import { useParams, useNavigate } from 'react-router-dom'; import { message } from 'antd'; import { ReactFlowProvider, useReactFlow } from '@xyflow/react'; +import { Loader2 } from 'lucide-react'; import WorkflowToolbar from './components/WorkflowToolbar'; import NodePanel from './components/NodePanel'; @@ -9,6 +10,7 @@ import FlowCanvas from './components/FlowCanvas'; import NodeConfigModal from './components/NodeConfigModal'; import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal'; import FormPreviewModal from './components/FormPreviewModal'; +import KeyboardShortcutsPanel from './components/KeyboardShortcutsPanel'; import type { FlowNode, FlowEdge, FlowNodeData } from './types'; import type { WorkflowNodeDefinition } from './nodes/types'; import { isConfigurableNode } from './nodes/types'; @@ -26,11 +28,11 @@ import './index.less'; const WorkflowDesignInner: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { - getNodes, - setNodes, - getEdges, - setEdges, + const { + getNodes, + setNodes, + getEdges, + setEdges, screenToFlowPosition, fitView, zoomIn, @@ -40,22 +42,28 @@ const WorkflowDesignInner: React.FC = () => { const [workflowTitle, setWorkflowTitle] = useState('新建工作流'); const [currentZoom, setCurrentZoom] = useState(1); // 当前缩放比例 + const [loading, setLoading] = useState(false); // 加载状态 + const [lastSaveTime, setLastSaveTime] = useState(null); // 最后保存时间 const reactFlowWrapper = useRef(null); - + const autoSaveTimerRef = useRef(null); + // 当前工作流ID const currentWorkflowId = id ? parseInt(id) : undefined; - + // 节点配置模态框状态 const [configModalVisible, setConfigModalVisible] = useState(false); const [configNode, setConfigNode] = useState(null); - + // 边配置模态框状态 const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false); const [configEdge, setConfigEdge] = useState(null); - + // 表单定义数据和预览弹窗状态 const [formDefinition, setFormDefinition] = useState(null); const [formPreviewVisible, setFormPreviewVisible] = useState(false); + + // 快捷键面板状态 + const [shortcutsPanelOpen, setShortcutsPanelOpen] = useState(false); // 提取表单字段(用于变量引用) const formFields = useMemo(() => { @@ -109,28 +117,33 @@ const WorkflowDesignInner: React.FC = () => { useEffect(() => { const loadData = async () => { if (currentWorkflowId) { - const data = await loadWorkflow(currentWorkflowId); - if (data) { - setNodes(data.nodes); - setEdges(data.edges); - setWorkflowTitle(data.definition?.name || '未命名工作流'); - - // 如果工作流关联了表单,加载表单定义数据 - if (data.definition?.formDefinitionId) { - try { - const formDef = await getFormDefinitionById(data.definition.formDefinitionId); - setFormDefinition(formDef); - console.log('表单定义数据已加载:', formDef); - } catch (error) { - console.error('加载表单定义失败:', error); + setLoading(true); + try { + const data = await loadWorkflow(currentWorkflowId); + if (data) { + setNodes(data.nodes); + setEdges(data.edges); + setWorkflowTitle(data.definition?.name || '未命名工作流'); + + // 如果工作流关联了表单,加载表单定义数据 + if (data.definition?.formDefinitionId) { + try { + const formDef = await getFormDefinitionById(data.definition.formDefinitionId); + setFormDefinition(formDef); + console.log('表单定义数据已加载:', formDef); + } catch (error) { + console.error('加载表单定义失败:', error); + } + } else { + setFormDefinition(null); } - } else { - setFormDefinition(null); } + } finally { + setLoading(false); } } }; - + loadData(); }, [currentWorkflowId, loadWorkflow, setNodes, setEdges]); @@ -161,10 +174,10 @@ const WorkflowDesignInner: React.FC = () => { const initialEdges: FlowEdge[] = []; // 工具栏事件处理 - const handleSave = useCallback(async () => { + const handleSave = useCallback(async (isAutoSave = false) => { const nodes = getNodes() as FlowNode[]; const edges = getEdges() as FlowEdge[]; - + const success = await saveWorkflow({ nodes, edges, @@ -173,12 +186,37 @@ const WorkflowDesignInner: React.FC = () => { description: workflowDefinition?.description || '', definitionData: workflowDefinition // 传递原始定义数据 }); - + if (success) { - console.log('保存工作流成功:', { nodes, edges }); + setLastSaveTime(new Date()); + if (!isAutoSave) { + console.log('保存工作流成功:', { nodes, edges }); + } } }, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]); + // 自动保存 + useEffect(() => { + if (!currentWorkflowId || !hasUnsavedChanges) return; + + // 清除之前的定时器 + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current); + } + + // 30秒后自动保存 + autoSaveTimerRef.current = setTimeout(() => { + handleSave(true); + message.success('已自动保存', 1); + }, 30000); + + return () => { + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current); + } + }; + }, [currentWorkflowId, hasUnsavedChanges, handleSave]); + const handleBack = useCallback(() => { navigate(-1); // 返回上一页,避免硬编码路径 }, [navigate]); @@ -543,24 +581,44 @@ const WorkflowDesignInner: React.FC = () => { handleDelete(); } } + // ? - 显示快捷键面板 + else if (e.key === '?' && !shouldSkipShortcut) { + e.preventDefault(); + setShortcutsPanelOpen(true); + } + // Ctrl+S / Cmd+S - 保存 + else if (ctrlKey && e.key === 's') { + e.preventDefault(); + handleSave(false); + } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete]); + }, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete, handleSave]); return ( -
+ {/* 加载状态 */} + {loading && ( +
+
+ +

加载工作流中...

+
+
+ )} + {/* 工具栏 */} handleSave(false)} onBack={handleBack} onPreviewForm={handlePreviewForm} hasFormDefinition={!!formDefinition} @@ -580,7 +638,7 @@ const WorkflowDesignInner: React.FC = () => { {/* 画布区域 */} -
@@ -624,6 +682,19 @@ const WorkflowDesignInner: React.FC = () => { onClose={() => setFormPreviewVisible(false)} formDefinition={formDefinition} /> + + {/* 快捷键面板 */} + setShortcutsPanelOpen(false)} + /> + + {/* 最后保存时间提示 */} + {lastSaveTime && ( +
+ 最后保存: {lastSaveTime.toLocaleTimeString()} +
+ )}
); };