重构前端逻辑
This commit is contained in:
parent
11c44bc95d
commit
fcdf005e22
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Background,
|
Background,
|
||||||
@ -80,24 +80,26 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||||||
// 节点变化处理
|
// 节点变化处理
|
||||||
const handleNodesChange = useCallback((changes: any) => {
|
const handleNodesChange = useCallback((changes: any) => {
|
||||||
onNodesStateChange(changes);
|
onNodesStateChange(changes);
|
||||||
if (onNodesChange) {
|
}, [onNodesStateChange]);
|
||||||
// 延迟获取最新状态 - 在实际项目中可以使用useEffect监听nodes变化
|
|
||||||
setTimeout(() => {
|
|
||||||
onNodesChange(nodes as FlowNode[]);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}, [onNodesStateChange, onNodesChange, nodes]);
|
|
||||||
|
|
||||||
// 边变化处理
|
// 边变化处理
|
||||||
const handleEdgesChange = useCallback((changes: any) => {
|
const handleEdgesChange = useCallback((changes: any) => {
|
||||||
onEdgesStateChange(changes);
|
onEdgesStateChange(changes);
|
||||||
if (onEdgesChange) {
|
}, [onEdgesStateChange]);
|
||||||
// 延迟获取最新状态 - 在实际项目中可以使用useEffect监听edges变化
|
|
||||||
setTimeout(() => {
|
// 使用 useEffect 监听 nodes 变化并通知父组件
|
||||||
onEdgesChange(edges as FlowEdge[]);
|
useEffect(() => {
|
||||||
}, 0);
|
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 的实时验证功能
|
// 连接验证 - 利用 React Flow 的实时验证功能
|
||||||
const isValidConnection = useCallback((connection: Connection | Edge) => {
|
const isValidConnection = useCallback((connection: Connection | Edge) => {
|
||||||
|
|||||||
@ -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<KeyboardShortcutsPanelProps> = ({ open, onClose }) => {
|
||||||
|
// 按分类分组快捷键
|
||||||
|
const groupedShortcuts = shortcuts.reduce((acc, shortcut) => {
|
||||||
|
if (!acc[shortcut.category]) {
|
||||||
|
acc[shortcut.category] = [];
|
||||||
|
}
|
||||||
|
acc[shortcut.category].push(shortcut);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, Shortcut[]>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Keyboard className="h-5 w-5" />
|
||||||
|
键盘快捷键
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogBody>
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{Object.entries(groupedShortcuts).map(([category, items]) => (
|
||||||
|
<div key={category}>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700 mb-3 pb-2 border-b">
|
||||||
|
{category}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{items.map((shortcut, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-600">{shortcut.description}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{shortcut.keys.map((key, keyIndex) => (
|
||||||
|
<React.Fragment key={keyIndex}>
|
||||||
|
{keyIndex > 0 && (
|
||||||
|
<span className="text-gray-400 text-xs mx-0.5">+</span>
|
||||||
|
)}
|
||||||
|
<kbd className="px-2 py-1 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-300 rounded shadow-sm">
|
||||||
|
{key}
|
||||||
|
</kbd>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-gray-500 text-center pt-4 mt-4 border-t">
|
||||||
|
提示:按 <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-gray-100 border border-gray-300 rounded">?</kbd> 键可随时打开此面板
|
||||||
|
</div>
|
||||||
|
</DialogBody>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyboardShortcutsPanel;
|
||||||
|
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { NODE_DEFINITIONS } from '../nodes';
|
import { NODE_DEFINITIONS } from '../nodes';
|
||||||
import { NodeCategory } from '../nodes/types';
|
import { NodeCategory } from '../nodes/types';
|
||||||
import type { WorkflowNodeDefinition } from '../nodes/types';
|
import type { WorkflowNodeDefinition } from '../nodes/types';
|
||||||
|
import styles from './NodePanel.module.css';
|
||||||
|
|
||||||
interface NodePanelProps {
|
interface NodePanelProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -31,59 +32,26 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
|
|||||||
<div
|
<div
|
||||||
key={nodeDefinition.nodeCode}
|
key={nodeDefinition.nodeCode}
|
||||||
draggable
|
draggable
|
||||||
style={{
|
className={styles.nodeItem}
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '8px',
|
|
||||||
padding: '8px 12px',
|
|
||||||
borderRadius: '6px',
|
|
||||||
border: '1px solid #e5e7eb',
|
|
||||||
background: 'white',
|
|
||||||
cursor: 'grab',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
marginBottom: '6px'
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = '#f9fafb';
|
|
||||||
e.currentTarget.style.borderColor = nodeDefinition.renderConfig.theme.primary;
|
e.currentTarget.style.borderColor = nodeDefinition.renderConfig.theme.primary;
|
||||||
e.currentTarget.style.transform = 'translateX(2px)';
|
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'white';
|
|
||||||
e.currentTarget.style.borderColor = '#e5e7eb';
|
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)}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div
|
||||||
fontSize: '16px',
|
className={styles.nodeIcon}
|
||||||
width: '20px',
|
style={{ color: nodeDefinition.renderConfig.theme.primary }}
|
||||||
textAlign: 'center',
|
>
|
||||||
color: nodeDefinition.renderConfig.theme.primary
|
|
||||||
}}>
|
|
||||||
{nodeDefinition.renderConfig.icon.content}
|
{nodeDefinition.renderConfig.icon.content}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className={styles.nodeInfo}>
|
||||||
<div style={{
|
<div className={styles.nodeName}>
|
||||||
fontSize: '13px',
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#374151',
|
|
||||||
lineHeight: '1.2'
|
|
||||||
}}>
|
|
||||||
{nodeDefinition.nodeName}
|
{nodeDefinition.nodeName}
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className={styles.nodeDesc}>
|
||||||
fontSize: '11px',
|
|
||||||
color: '#6b7280',
|
|
||||||
lineHeight: '1.2',
|
|
||||||
marginTop: '2px'
|
|
||||||
}}>
|
|
||||||
{nodeDefinition.description}
|
{nodeDefinition.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -99,72 +67,30 @@ const NodePanel: React.FC<NodePanelProps> = ({ className = '' }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`node-panel ${className}`} style={{
|
<div className={`${styles.nodePanel} ${className}`}>
|
||||||
width: '260px',
|
<div className={styles.header}>
|
||||||
height: '100%',
|
<h3 className={styles.title}>节点面板</h3>
|
||||||
background: '#f8fafc',
|
<p className={styles.subtitle}>拖拽节点到画布创建工作流</p>
|
||||||
borderRight: '1px solid #e5e7eb',
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
padding: '16px',
|
|
||||||
background: 'white',
|
|
||||||
borderBottom: '1px solid #e5e7eb'
|
|
||||||
}}>
|
|
||||||
<h3 style={{
|
|
||||||
margin: 0,
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#374151'
|
|
||||||
}}>
|
|
||||||
节点面板
|
|
||||||
</h3>
|
|
||||||
<p style={{
|
|
||||||
margin: '4px 0 0 0',
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#6b7280'
|
|
||||||
}}>
|
|
||||||
拖拽节点到画布创建工作流
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div className={styles.content}>
|
||||||
flex: '1',
|
|
||||||
overflow: 'auto',
|
|
||||||
padding: '12px'
|
|
||||||
}}>
|
|
||||||
{Object.entries(nodesByCategory).map(([category, nodes]) => (
|
{Object.entries(nodesByCategory).map(([category, nodes]) => (
|
||||||
<div key={category} style={{ marginBottom: '16px' }}>
|
<div key={category} className={styles.category}>
|
||||||
<div style={{
|
<div className={styles.categoryTitle}>
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#4b5563',
|
|
||||||
marginBottom: '8px',
|
|
||||||
padding: '4px 0',
|
|
||||||
borderBottom: '1px solid #e5e7eb'
|
|
||||||
}}>
|
|
||||||
{categoryTitles[category as NodeCategory]}
|
{categoryTitles[category as NodeCategory]}
|
||||||
</div>
|
</div>
|
||||||
{nodes.map(renderNodeItem)}
|
{nodes.map(renderNodeItem)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 使用提示 */}
|
{/* 使用提示 */}
|
||||||
<div style={{
|
<div className={styles.tips}>
|
||||||
padding: '12px',
|
|
||||||
background: '#f1f5f9',
|
|
||||||
borderTop: '1px solid #e5e7eb',
|
|
||||||
fontSize: '11px',
|
|
||||||
color: '#64748b',
|
|
||||||
lineHeight: '1.4'
|
|
||||||
}}>
|
|
||||||
💡 提示:
|
💡 提示:
|
||||||
<br />• 拖拽节点到画布创建
|
<br />• 拖拽节点到画布创建
|
||||||
<br />• 双击节点进行配置
|
<br />• 双击节点进行配置
|
||||||
<br />• 连接节点创建流程
|
<br />• 连接节点创建流程
|
||||||
|
<br />• 按 ? 查看快捷键
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'
|
|||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
|
import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
import WorkflowToolbar from './components/WorkflowToolbar';
|
import WorkflowToolbar from './components/WorkflowToolbar';
|
||||||
import NodePanel from './components/NodePanel';
|
import NodePanel from './components/NodePanel';
|
||||||
@ -9,6 +10,7 @@ import FlowCanvas from './components/FlowCanvas';
|
|||||||
import NodeConfigModal from './components/NodeConfigModal';
|
import NodeConfigModal from './components/NodeConfigModal';
|
||||||
import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal';
|
import EdgeConfigModal, { type EdgeCondition } from './components/EdgeConfigModal';
|
||||||
import FormPreviewModal from './components/FormPreviewModal';
|
import FormPreviewModal from './components/FormPreviewModal';
|
||||||
|
import KeyboardShortcutsPanel from './components/KeyboardShortcutsPanel';
|
||||||
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
|
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
|
||||||
import type { WorkflowNodeDefinition } from './nodes/types';
|
import type { WorkflowNodeDefinition } from './nodes/types';
|
||||||
import { isConfigurableNode } from './nodes/types';
|
import { isConfigurableNode } from './nodes/types';
|
||||||
@ -26,11 +28,11 @@ import './index.less';
|
|||||||
const WorkflowDesignInner: React.FC = () => {
|
const WorkflowDesignInner: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
getNodes,
|
getNodes,
|
||||||
setNodes,
|
setNodes,
|
||||||
getEdges,
|
getEdges,
|
||||||
setEdges,
|
setEdges,
|
||||||
screenToFlowPosition,
|
screenToFlowPosition,
|
||||||
fitView,
|
fitView,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
@ -40,22 +42,28 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
|
|
||||||
const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
|
const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
|
||||||
const [currentZoom, setCurrentZoom] = useState(1); // 当前缩放比例
|
const [currentZoom, setCurrentZoom] = useState(1); // 当前缩放比例
|
||||||
|
const [loading, setLoading] = useState(false); // 加载状态
|
||||||
|
const [lastSaveTime, setLastSaveTime] = useState<Date | null>(null); // 最后保存时间
|
||||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// 当前工作流ID
|
// 当前工作流ID
|
||||||
const currentWorkflowId = id ? parseInt(id) : undefined;
|
const currentWorkflowId = id ? parseInt(id) : undefined;
|
||||||
|
|
||||||
// 节点配置模态框状态
|
// 节点配置模态框状态
|
||||||
const [configModalVisible, setConfigModalVisible] = useState(false);
|
const [configModalVisible, setConfigModalVisible] = useState(false);
|
||||||
const [configNode, setConfigNode] = useState<FlowNode | null>(null);
|
const [configNode, setConfigNode] = useState<FlowNode | null>(null);
|
||||||
|
|
||||||
// 边配置模态框状态
|
// 边配置模态框状态
|
||||||
const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false);
|
const [edgeConfigModalVisible, setEdgeConfigModalVisible] = useState(false);
|
||||||
const [configEdge, setConfigEdge] = useState<FlowEdge | null>(null);
|
const [configEdge, setConfigEdge] = useState<FlowEdge | null>(null);
|
||||||
|
|
||||||
// 表单定义数据和预览弹窗状态
|
// 表单定义数据和预览弹窗状态
|
||||||
const [formDefinition, setFormDefinition] = useState<FormDefinitionResponse | null>(null);
|
const [formDefinition, setFormDefinition] = useState<FormDefinitionResponse | null>(null);
|
||||||
const [formPreviewVisible, setFormPreviewVisible] = useState(false);
|
const [formPreviewVisible, setFormPreviewVisible] = useState(false);
|
||||||
|
|
||||||
|
// 快捷键面板状态
|
||||||
|
const [shortcutsPanelOpen, setShortcutsPanelOpen] = useState(false);
|
||||||
|
|
||||||
// 提取表单字段(用于变量引用)
|
// 提取表单字段(用于变量引用)
|
||||||
const formFields = useMemo(() => {
|
const formFields = useMemo(() => {
|
||||||
@ -109,28 +117,33 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
if (currentWorkflowId) {
|
if (currentWorkflowId) {
|
||||||
const data = await loadWorkflow(currentWorkflowId);
|
setLoading(true);
|
||||||
if (data) {
|
try {
|
||||||
setNodes(data.nodes);
|
const data = await loadWorkflow(currentWorkflowId);
|
||||||
setEdges(data.edges);
|
if (data) {
|
||||||
setWorkflowTitle(data.definition?.name || '未命名工作流');
|
setNodes(data.nodes);
|
||||||
|
setEdges(data.edges);
|
||||||
// 如果工作流关联了表单,加载表单定义数据
|
setWorkflowTitle(data.definition?.name || '未命名工作流');
|
||||||
if (data.definition?.formDefinitionId) {
|
|
||||||
try {
|
// 如果工作流关联了表单,加载表单定义数据
|
||||||
const formDef = await getFormDefinitionById(data.definition.formDefinitionId);
|
if (data.definition?.formDefinitionId) {
|
||||||
setFormDefinition(formDef);
|
try {
|
||||||
console.log('表单定义数据已加载:', formDef);
|
const formDef = await getFormDefinitionById(data.definition.formDefinitionId);
|
||||||
} catch (error) {
|
setFormDefinition(formDef);
|
||||||
console.error('加载表单定义失败:', error);
|
console.log('表单定义数据已加载:', formDef);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表单定义失败:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFormDefinition(null);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
setFormDefinition(null);
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
}, [currentWorkflowId, loadWorkflow, setNodes, setEdges]);
|
}, [currentWorkflowId, loadWorkflow, setNodes, setEdges]);
|
||||||
|
|
||||||
@ -161,10 +174,10 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
const initialEdges: FlowEdge[] = [];
|
const initialEdges: FlowEdge[] = [];
|
||||||
|
|
||||||
// 工具栏事件处理
|
// 工具栏事件处理
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async (isAutoSave = false) => {
|
||||||
const nodes = getNodes() as FlowNode[];
|
const nodes = getNodes() as FlowNode[];
|
||||||
const edges = getEdges() as FlowEdge[];
|
const edges = getEdges() as FlowEdge[];
|
||||||
|
|
||||||
const success = await saveWorkflow({
|
const success = await saveWorkflow({
|
||||||
nodes,
|
nodes,
|
||||||
edges,
|
edges,
|
||||||
@ -173,12 +186,37 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
description: workflowDefinition?.description || '',
|
description: workflowDefinition?.description || '',
|
||||||
definitionData: workflowDefinition // 传递原始定义数据
|
definitionData: workflowDefinition // 传递原始定义数据
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log('保存工作流成功:', { nodes, edges });
|
setLastSaveTime(new Date());
|
||||||
|
if (!isAutoSave) {
|
||||||
|
console.log('保存工作流成功:', { nodes, edges });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
|
}, [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(() => {
|
const handleBack = useCallback(() => {
|
||||||
navigate(-1); // 返回上一页,避免硬编码路径
|
navigate(-1); // 返回上一页,避免硬编码路径
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@ -543,24 +581,44 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
handleDelete();
|
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);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete]);
|
}, [handleUndo, handleRedo, handleCopy, handlePaste, handleSelectAll, handleDelete, handleSave]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="workflow-design-container"
|
className="workflow-design-container"
|
||||||
style={{
|
style={{
|
||||||
// 确保覆盖父容器的overflow设置
|
// 确保覆盖父容器的overflow设置
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 加载状态 */}
|
||||||
|
{loading && (
|
||||||
|
<div className="absolute inset-0 bg-white/80 backdrop-blur-sm z-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-primary mx-auto mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground">加载工作流中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<WorkflowToolbar
|
<WorkflowToolbar
|
||||||
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
|
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
|
||||||
onSave={handleSave}
|
onSave={() => handleSave(false)}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
onPreviewForm={handlePreviewForm}
|
onPreviewForm={handlePreviewForm}
|
||||||
hasFormDefinition={!!formDefinition}
|
hasFormDefinition={!!formDefinition}
|
||||||
@ -580,7 +638,7 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
<NodePanel />
|
<NodePanel />
|
||||||
|
|
||||||
{/* 画布区域 */}
|
{/* 画布区域 */}
|
||||||
<div
|
<div
|
||||||
ref={reactFlowWrapper}
|
ref={reactFlowWrapper}
|
||||||
className="workflow-canvas-area"
|
className="workflow-canvas-area"
|
||||||
>
|
>
|
||||||
@ -624,6 +682,19 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
onClose={() => setFormPreviewVisible(false)}
|
onClose={() => setFormPreviewVisible(false)}
|
||||||
formDefinition={formDefinition}
|
formDefinition={formDefinition}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 快捷键面板 */}
|
||||||
|
<KeyboardShortcutsPanel
|
||||||
|
open={shortcutsPanelOpen}
|
||||||
|
onClose={() => setShortcutsPanelOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 最后保存时间提示 */}
|
||||||
|
{lastSaveTime && (
|
||||||
|
<div className="fixed bottom-4 right-4 bg-white border border-gray-200 rounded-lg shadow-sm px-3 py-2 text-xs text-gray-600 z-10">
|
||||||
|
最后保存: {lastSaveTime.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user