import React, { useState, useCallback, useRef, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import ReactFlow, { Node, Edge, addEdge, Connection, useNodesState, useEdgesState, Controls, Background, MiniMap, NodeTypes, BackgroundVariant, Handle, Position, ConnectionMode, EdgeLabelRenderer, BaseEdge, getStraightPath, EdgeTypes, } from 'reactflow'; import 'reactflow/dist/style.css'; import { SaveOutlined, ArrowLeftOutlined, UndoOutlined, RedoOutlined, ToolOutlined, CheckCircleOutlined, FileTextOutlined, EditOutlined, SafetyOutlined, UnlockOutlined, LockOutlined, CheckSquareOutlined, CloseOutlined, ZoomInOutlined, ZoomOutOutlined, // 更多图标用于选择器 HomeOutlined, UserOutlined, TeamOutlined, SettingOutlined, FolderOutlined, FileOutlined, FolderOpenOutlined, DatabaseOutlined, CloudOutlined, ThunderboltOutlined, FireOutlined, RocketOutlined, StarOutlined, HeartOutlined, BellOutlined, MessageOutlined, PhoneOutlined, MailOutlined, CalendarOutlined, ClockCircleOutlined, SearchOutlined, PlusOutlined, MinusOutlined, DeleteOutlined, EditOutlined as EditIconOutlined, EyeOutlined, EyeInvisibleOutlined, DownloadOutlined, UploadOutlined, ReloadOutlined, PlayCircleOutlined, PauseCircleOutlined, StopOutlined, CheckOutlined, CloseCircleOutlined, WarningOutlined, InfoCircleOutlined, QuestionCircleOutlined, LinkOutlined, ShareAltOutlined, CopyOutlined, ScissorOutlined, PrinterOutlined, ShoppingCartOutlined, ShoppingOutlined, GiftOutlined, TrophyOutlined, CrownOutlined, BulbOutlined, ExperimentOutlined, BugOutlined, CodeOutlined, ApiOutlined, AppstoreOutlined, BarsOutlined, MenuOutlined, LayoutOutlined, TableOutlined, UnorderedListOutlined, OrderedListOutlined, PictureOutlined, VideoCameraOutlined, SoundOutlined, CustomerServiceOutlined, GlobalOutlined, EnvironmentOutlined, CompassOutlined, CarOutlined, BankOutlined, ShopOutlined, MedicineBoxOutlined, SafetyCertificateOutlined, InsuranceOutlined, FileProtectOutlined, FileSyncOutlined, FileSearchOutlined, FileAddOutlined, FileExcelOutlined, FilePdfOutlined, FileWordOutlined, FileImageOutlined, FileZipOutlined, FolderAddOutlined, FolderViewOutlined, ProjectOutlined, BuildOutlined, ToolOutlined as ToolIconOutlined, RobotOutlined, BugOutlined as BugIconOutlined, ExperimentOutlined as ExperimentIconOutlined, FireOutlined as FireIconOutlined, ThunderboltOutlined as ThunderboltIconOutlined, } from '@ant-design/icons'; import { Button, Input, Select, Checkbox, Tabs, Modal, Dropdown, Popover, message } from 'antd'; import type { MenuProps } from 'antd'; import { toast } from 'sonner'; import { workflowDesignApi, WorkflowDesignVO } from '../api/WorkflowDesign'; import { userApi } from '../api/user'; import { UserVO } from '../types'; import { segregationPointApi, SegregationPointVO } from '../api/spm'; import { getFormPage, FormVO } from '../api/bpm/form'; // 节点配置 const nodeConfigs = [ { type: 'createJob', label: '创建作业', icon: ToolOutlined, bgColor: 'bg-white', bgColorCustom: '#ffffff', iconColor: 'text-blue-600', iconColorCustom: '#165dff', borderColor: 'border-blue-100', }, { type: 'confirm', label: '确认', icon: CheckCircleOutlined, bgColor: 'bg-white', bgColorCustom: '#ffffff', iconColor: 'text-green-600', iconColorCustom: '#36d399', borderColor: 'border-green-100', }, { type: 'review', label: '审核', icon: FileTextOutlined, bgColor: 'bg-white', bgColorCustom: '#ffffff', iconColor: 'text-orange-600', iconColorCustom: '#fb923c', borderColor: 'border-orange-100', }, { type: 'inputInfo', label: '录入信息', icon: EditOutlined, bgColor: 'bg-white', bgColorCustom: '#ffffff', iconColor: 'text-purple-600', iconColorCustom: '#9665ff', borderColor: 'border-purple-100', }, { type: 'isolation', label: '隔离/方案', icon: SafetyOutlined, bgColor: 'bg-white', bgColorCustom: '#ffffff', iconColor: 'text-red-600', iconColorCustom: '#f87272', borderColor: 'border-red-100', }, { type: 'releaseIsolation', label: '解除隔离', icon: UnlockOutlined, bgColor: 'bg-white', bgColorCustom: '#ffffff', iconColor: 'text-yellow-600', iconColorCustom: '#38bdf8', borderColor: 'border-yellow-100', }, { type: 'returnLock', label: '还锁', icon: LockOutlined, bgColor: 'bg-white', bgColorCustom: '#ffffff', iconColor: 'text-indigo-600', iconColorCustom: '#6b7280', borderColor: 'border-indigo-100', }, { type: 'complete', label: '完成/结束', icon: CheckSquareOutlined, bgColor: 'bg-white', bgColorCustom: '#ffffff', iconColor: 'text-gray-600', iconColorCustom: '#10b981', borderColor: 'border-gray-100', }, ]; // 可选的图标列表(用于图标选择器) const availableIcons = [ { name: 'ToolOutlined', component: ToolOutlined, label: '工具' }, { name: 'CheckCircleOutlined', component: CheckCircleOutlined, label: '确认' }, { name: 'FileTextOutlined', component: FileTextOutlined, label: '文件' }, { name: 'EditOutlined', component: EditOutlined, label: '编辑' }, { name: 'SafetyOutlined', component: SafetyOutlined, label: '安全' }, { name: 'UnlockOutlined', component: UnlockOutlined, label: '解锁' }, { name: 'LockOutlined', component: LockOutlined, label: '锁定' }, { name: 'CheckSquareOutlined', component: CheckSquareOutlined, label: '完成' }, { name: 'HomeOutlined', component: HomeOutlined, label: '首页' }, { name: 'UserOutlined', component: UserOutlined, label: '用户' }, { name: 'TeamOutlined', component: TeamOutlined, label: '团队' }, { name: 'SettingOutlined', component: SettingOutlined, label: '设置' }, { name: 'FolderOutlined', component: FolderOutlined, label: '文件夹' }, { name: 'FileOutlined', component: FileOutlined, label: '文件' }, { name: 'FolderOpenOutlined', component: FolderOpenOutlined, label: '打开文件夹' }, { name: 'DatabaseOutlined', component: DatabaseOutlined, label: '数据库' }, { name: 'CloudOutlined', component: CloudOutlined, label: '云' }, { name: 'ThunderboltOutlined', component: ThunderboltOutlined, label: '闪电' }, { name: 'FireOutlined', component: FireOutlined, label: '火焰' }, { name: 'RocketOutlined', component: RocketOutlined, label: '火箭' }, { name: 'StarOutlined', component: StarOutlined, label: '星星' }, { name: 'HeartOutlined', component: HeartOutlined, label: '心形' }, { name: 'BellOutlined', component: BellOutlined, label: '铃铛' }, { name: 'MessageOutlined', component: MessageOutlined, label: '消息' }, { name: 'PhoneOutlined', component: PhoneOutlined, label: '电话' }, { name: 'MailOutlined', component: MailOutlined, label: '邮件' }, { name: 'CalendarOutlined', component: CalendarOutlined, label: '日历' }, { name: 'ClockCircleOutlined', component: ClockCircleOutlined, label: '时钟' }, { name: 'SearchOutlined', component: SearchOutlined, label: '搜索' }, { name: 'PlusOutlined', component: PlusOutlined, label: '添加' }, { name: 'MinusOutlined', component: MinusOutlined, label: '减少' }, { name: 'DeleteOutlined', component: DeleteOutlined, label: '删除' }, { name: 'EyeOutlined', component: EyeOutlined, label: '查看' }, { name: 'EyeInvisibleOutlined', component: EyeInvisibleOutlined, label: '隐藏' }, { name: 'DownloadOutlined', component: DownloadOutlined, label: '下载' }, { name: 'UploadOutlined', component: UploadOutlined, label: '上传' }, { name: 'ReloadOutlined', component: ReloadOutlined, label: '刷新' }, { name: 'PlayCircleOutlined', component: PlayCircleOutlined, label: '播放' }, { name: 'PauseCircleOutlined', component: PauseCircleOutlined, label: '暂停' }, { name: 'StopOutlined', component: StopOutlined, label: '停止' }, { name: 'CheckOutlined', component: CheckOutlined, label: '勾选' }, { name: 'CloseCircleOutlined', component: CloseCircleOutlined, label: '关闭' }, { name: 'WarningOutlined', component: WarningOutlined, label: '警告' }, { name: 'InfoCircleOutlined', component: InfoCircleOutlined, label: '信息' }, { name: 'QuestionCircleOutlined', component: QuestionCircleOutlined, label: '问号' }, { name: 'LinkOutlined', component: LinkOutlined, label: '链接' }, { name: 'ShareAltOutlined', component: ShareAltOutlined, label: '分享' }, { name: 'CopyOutlined', component: CopyOutlined, label: '复制' }, { name: 'ScissorOutlined', component: ScissorOutlined, label: '剪切' }, { name: 'PrinterOutlined', component: PrinterOutlined, label: '打印' }, { name: 'ShoppingCartOutlined', component: ShoppingCartOutlined, label: '购物车' }, { name: 'ShoppingOutlined', component: ShoppingOutlined, label: '商店' }, { name: 'GiftOutlined', component: GiftOutlined, label: '礼物' }, { name: 'TrophyOutlined', component: TrophyOutlined, label: '奖杯' }, { name: 'CrownOutlined', component: CrownOutlined, label: '皇冠' }, { name: 'BulbOutlined', component: BulbOutlined, label: '灯泡' }, { name: 'ExperimentOutlined', component: ExperimentOutlined, label: '实验' }, { name: 'BugOutlined', component: BugOutlined, label: '错误' }, { name: 'CodeOutlined', component: CodeOutlined, label: '代码' }, { name: 'ApiOutlined', component: ApiOutlined, label: 'API' }, { name: 'AppstoreOutlined', component: AppstoreOutlined, label: '应用' }, { name: 'BarsOutlined', component: BarsOutlined, label: '菜单' }, { name: 'MenuOutlined', component: MenuOutlined, label: '菜单' }, { name: 'LayoutOutlined', component: LayoutOutlined, label: '布局' }, { name: 'TableOutlined', component: TableOutlined, label: '表格' }, { name: 'UnorderedListOutlined', component: UnorderedListOutlined, label: '列表' }, { name: 'OrderedListOutlined', component: OrderedListOutlined, label: '有序列表' }, { name: 'PictureOutlined', component: PictureOutlined, label: '图片' }, { name: 'VideoCameraOutlined', component: VideoCameraOutlined, label: '视频' }, { name: 'SoundOutlined', component: SoundOutlined, label: '声音' }, { name: 'CustomerServiceOutlined', component: CustomerServiceOutlined, label: '客服' }, { name: 'GlobalOutlined', component: GlobalOutlined, label: '全球' }, { name: 'EnvironmentOutlined', component: EnvironmentOutlined, label: '位置' }, { name: 'CompassOutlined', component: CompassOutlined, label: '指南针' }, { name: 'CarOutlined', component: CarOutlined, label: '汽车' }, { name: 'BankOutlined', component: BankOutlined, label: '银行' }, { name: 'ShopOutlined', component: ShopOutlined, label: '商店' }, { name: 'MedicineBoxOutlined', component: MedicineBoxOutlined, label: '医药' }, { name: 'SafetyCertificateOutlined', component: SafetyCertificateOutlined, label: '证书' }, { name: 'InsuranceOutlined', component: InsuranceOutlined, label: '保险' }, { name: 'FileProtectOutlined', component: FileProtectOutlined, label: '保护' }, { name: 'FileSyncOutlined', component: FileSyncOutlined, label: '同步' }, { name: 'FileSearchOutlined', component: FileSearchOutlined, label: '搜索文件' }, { name: 'FileAddOutlined', component: FileAddOutlined, label: '添加文件' }, { name: 'FileExcelOutlined', component: FileExcelOutlined, label: 'Excel' }, { name: 'FilePdfOutlined', component: FilePdfOutlined, label: 'PDF' }, { name: 'FileWordOutlined', component: FileWordOutlined, label: 'Word' }, { name: 'FileImageOutlined', component: FileImageOutlined, label: '图片文件' }, { name: 'FileZipOutlined', component: FileZipOutlined, label: '压缩包' }, { name: 'FolderAddOutlined', component: FolderAddOutlined, label: '添加文件夹' }, { name: 'FolderViewOutlined', component: FolderViewOutlined, label: '查看文件夹' }, { name: 'ProjectOutlined', component: ProjectOutlined, label: '项目' }, { name: 'BuildOutlined', component: BuildOutlined, label: '构建' }, { name: 'RobotOutlined', component: RobotOutlined, label: '机器人' }, ]; // 图标名称到组件的映射 const iconNameMap: Record> = { ToolOutlined, CheckCircleOutlined, FileTextOutlined, EditOutlined, SafetyOutlined, UnlockOutlined, LockOutlined, CheckSquareOutlined, HomeOutlined, UserOutlined, TeamOutlined, SettingOutlined, FolderOutlined, FileOutlined, FolderOpenOutlined, DatabaseOutlined, CloudOutlined, ThunderboltOutlined, FireOutlined, RocketOutlined, StarOutlined, HeartOutlined, BellOutlined, MessageOutlined, PhoneOutlined, MailOutlined, CalendarOutlined, ClockCircleOutlined, SearchOutlined, PlusOutlined, MinusOutlined, DeleteOutlined, EyeOutlined, EyeInvisibleOutlined, DownloadOutlined, UploadOutlined, ReloadOutlined, PlayCircleOutlined, PauseCircleOutlined, StopOutlined, CheckOutlined, CloseCircleOutlined, WarningOutlined, InfoCircleOutlined, QuestionCircleOutlined, LinkOutlined, ShareAltOutlined, CopyOutlined, ScissorOutlined, PrinterOutlined, ShoppingCartOutlined, ShoppingOutlined, GiftOutlined, TrophyOutlined, CrownOutlined, BulbOutlined, ExperimentOutlined, BugOutlined, CodeOutlined, ApiOutlined, AppstoreOutlined, BarsOutlined, MenuOutlined, LayoutOutlined, TableOutlined, UnorderedListOutlined, OrderedListOutlined, PictureOutlined, VideoCameraOutlined, SoundOutlined, CustomerServiceOutlined, GlobalOutlined, EnvironmentOutlined, CompassOutlined, CarOutlined, BankOutlined, ShopOutlined, MedicineBoxOutlined, SafetyCertificateOutlined, InsuranceOutlined, FileProtectOutlined, FileSyncOutlined, FileSearchOutlined, FileAddOutlined, FileExcelOutlined, FilePdfOutlined, FileWordOutlined, FileImageOutlined, FileZipOutlined, FolderAddOutlined, FolderViewOutlined, ProjectOutlined, BuildOutlined, RobotOutlined, }; // 自定义节点组件 function CustomNode({ data, selected, id }: any) { // 优先使用自定义图标,否则使用节点类型对应的图标 let Icon = FileTextOutlined; let config = null; if (data.icon && iconNameMap[data.icon]) { // 使用自定义图标 Icon = iconNameMap[data.icon]; // 尝试找到对应的配置(保持颜色等样式) config = nodeConfigs.find(c => c.icon === Icon); if (!config) { // 如果找不到配置,使用默认配置 config = nodeConfigs.find(c => c.type === data.type) || nodeConfigs[0]; } } else { // 使用节点类型对应的图标 config = nodeConfigs.find(c => c.type === data.type); Icon = config?.icon || FileTextOutlined; } // 从节点ID中提取序号,或使用data中的nodeId const nodeId = data.nodeId || (id ? String(parseInt(id.split('-').pop() || '0') % 1000).padStart(3, '0') : '001'); return (
{/* 连接点 - 四个方向,每个方向都有唯一的 source 和 target Handle */} {/* 垂直布局:顶部图标、中间名称、底部ID */}
{/* 顶部:图标 */}
{/* 中间:节点名称 */}
{data.label || config?.label}
{/* 底部:ID */}
ID: {nodeId}
); } // 各个节点类型 function CreateJobNode(props: any) { return ; } function ConfirmNode(props: any) { return ; } function ReviewNode(props: any) { return ; } function InputInfoNode(props: any) { return ; } function IsolationNode(props: any) { return ; } function ReleaseIsolationNode(props: any) { return ; } function ReturnLockNode(props: any) { return ; } function CompleteNode(props: any) { return ; } // 自定义节点类型映射 const nodeTypes = { createJob: CreateJobNode, confirm: ConfirmNode, review: ReviewNode, inputInfo: InputInfoNode, isolation: IsolationNode, releaseIsolation: ReleaseIsolationNode, returnLock: ReturnLockNode, complete: CompleteNode, }; // 全局变量存储删除函数(用于边组件) let globalDeleteEdgeFn: ((id: string) => void) | null = null; // 自定义边组件 - 定义在组件外部以确保稳定引用 function CustomEdgeWithDelete({ id, sourceX, sourceY, targetX, targetY, selected, markerEnd, style, }: any) { const [edgePath, labelX, labelY] = getStraightPath({ sourceX, sourceY, targetX, targetY, }); console.log('边组件渲染,ID:', id, 'selected:', selected, 'labelX:', labelX, 'labelY:', labelY); return ( <> {selected && (
)} ); } // 自定义边类型映射 - 定义在组件外部 const edgeTypes: EdgeTypes = { straight: CustomEdgeWithDelete, default: CustomEdgeWithDelete, }; export default function ProcessDesigner() { const navigate = useNavigate(); const location = useLocation(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); // 从URL参数获取流程ID const workflowId = React.useMemo(() => { const params = new URLSearchParams(location.search); const id = params.get('id'); return id ? parseInt(id, 10) : null; }, [location.search]); const [selectedNode, setSelectedNode] = useState(null); const [selectedEdge, setSelectedEdge] = useState(null); const [activeTabKey, setActiveTabKey] = useState('info'); const reactFlowWrapper = useRef(null); const [reactFlowInstance, setReactFlowInstance] = useState(null); const [zoom, setZoom] = useState(1); const [iconPickerOpen, setIconPickerOpen] = useState(false); // 简单缓存:将每个节点的配置持久化到 sessionStorage,key 按节点ID const cachePrefix = 'process_designer_node_'; const [exportVisible, setExportVisible] = useState(false); const [exportContent, setExportContent] = useState(''); const [importVisible, setImportVisible] = useState(false); const [importJson, setImportJson] = useState(''); // 存储工作流详情(name、description等) const [workflowDetail, setWorkflowDetail] = useState(null); const [loadingDetail, setLoadingDetail] = useState(false); // 角色用户列表 const [drawerUsers, setDrawerUsers] = useState([]); // 负责人(jtdrawer) const [lockerUsers, setLockerUsers] = useState([]); // 上锁人(jtlocker) const [colockerUsers, setColockerUsers] = useState([]); // 共锁人(jtcolocker) // 隔离点列表 const [isolationPoints, setIsolationPoints] = useState([]); // 表单列表 const [formList, setFormList] = useState([]); const loadNodeCache = useCallback((nodeId: string) => { try { const raw = sessionStorage.getItem(`${cachePrefix}${nodeId}`); if (!raw) return null; return JSON.parse(raw); } catch { return null; } }, []); const saveNodeCache = useCallback((nodeId: string, data: any) => { try { sessionStorage.setItem(`${cachePrefix}${nodeId}`, JSON.stringify(data)); } catch { // ignore } }, []); // 调试:监听 edges 变化 useEffect(() => { console.log('Edges 状态更新:', edges.length, edges); }, [edges]); // 加载角色用户列表和隔离点列表 useEffect(() => { const loadRoleUsers = async () => { try { // 并行加载三种角色的用户 const [drawerRes, lockerRes, colockerRes] = await Promise.all([ userApi.getRoleUser('jtdrawer'), userApi.getRoleUser('jtlocker'), userApi.getRoleUser('jtcolocker'), ]); setDrawerUsers(drawerRes || []); setLockerUsers(lockerRes || []); setColockerUsers(colockerRes || []); } catch (error) { console.error('加载角色用户失败:', error); } }; const loadIsolationPoints = async () => { try { const res = await segregationPointApi.getIsIsolationPointPage({ pageNo: 1, pageSize: -1 }); setIsolationPoints(res.list || []); } catch (error) { console.error('加载隔离点列表失败:', error); } }; const loadFormList = async () => { try { const res = await getFormPage({ pageNo: 1, pageSize: -1 }); setFormList(res.list || []); } catch (error) { console.error('加载表单列表失败:', error); } }; loadRoleUsers(); loadIsolationPoints(); loadFormList(); }, []); // 节点配置状态 const [nodeConfig, setNodeConfig] = useState({ nodeName: '', nodeIcon: '', responsible: '', remark: '', submitForm: '', isolationMethod: '', // 隔离方式 isolationPoints: [] as string[], // 隔离点选择(多选) isolationNode: [] as string[], // 隔离节点(多选) selectedIsolationNodeId: '', // 选择的隔离/方案节点ID(用于解除隔离节点) lockPerson: '', // 上锁人 coLockPersons: [] as string[], // 共锁人(多选) notificationMethods: { sms: false, message: false, email: false, app: false, }, notificationPerson: '', notificationTime: '', }); // 实时缓存并更新节点配置,避免切换节点后丢失未保存的输入 useEffect(() => { if (selectedNode) { const updatedData = { ...selectedNode.data, label: nodeConfig.nodeName, icon: nodeConfig.nodeIcon, responsible: nodeConfig.responsible, remark: nodeConfig.remark, submitForm: nodeConfig.submitForm, isolationMethod: nodeConfig.isolationMethod, isolationPoints: nodeConfig.isolationPoints, isolationNode: nodeConfig.isolationNode, selectedIsolationNodeId: nodeConfig.selectedIsolationNodeId, lockPerson: nodeConfig.lockPerson, coLockPersons: nodeConfig.coLockPersons, notificationMethods: nodeConfig.notificationMethods, notificationPerson: nodeConfig.notificationPerson, notificationTime: nodeConfig.notificationTime, nodeName: nodeConfig.nodeName, nodeIcon: nodeConfig.nodeIcon, }; // 缓存配置 saveNodeCache(selectedNode.id, updatedData); // 实时更新节点显示 setNodes((nds) => nds.map((node) => node.id === selectedNode.id ? { ...node, data: updatedData } : node ) ); } }, [nodeConfig, selectedNode, saveNodeCache, setNodes]); // 历史记录(用于撤销/重做) const [history, setHistory] = useState<{ nodes: Node[]; edges: Edge[] }[]>([]); const [historyIndex, setHistoryIndex] = useState(-1); // 加载 JSON 数据到画布(可复用的函数) const loadJsonToCanvas = useCallback((jsonString: string, updateHistory: boolean = false) => { try { const data = JSON.parse(jsonString); if (!data.nodes || !Array.isArray(data.nodes)) { message.error('JSON 格式错误:缺少 nodes 数组'); return false; } if (!data.edges || !Array.isArray(data.edges)) { message.error('JSON 格式错误:缺少 edges 数组'); return false; } // 创建uuid到id的映射(导入时uuid改回id) const uuidToIdMap = new Map(); data.nodes.forEach((node: any) => { // 兼容处理:支持uuid字段或id字段 const nodeId = node.uuid || node.id; uuidToIdMap.set(nodeId, nodeId); // 值保持不变 }); // 还原节点,确保 data 包含所有配置字段 const importedNodes = data.nodes.map((node: any) => { const nodeData = node.data || {}; // 兼容处理:支持uuid字段或id字段 const nodeId = node.uuid || node.id; // 如果顶层有 nodeName 和 nodeIcon,将它们还原到 data 中 const topLevelLabel = node.nodeName || ''; const topLevelIcon = node.nodeIcon || ''; // 确保所有字段都存在,使用默认值填充缺失的字段 const completeData = { label: topLevelLabel || nodeData.label || '', // 优先使用顶层的 nodeName type: nodeData.type || node.type, nodeId: nodeData.nodeId || '', icon: topLevelIcon || nodeData.icon || nodeData.type || node.type, // 优先使用顶层的 nodeIcon responsible: nodeData.responsible || '', remark: nodeData.remark || '', submitForm: nodeData.submitForm || '', isolationMethod: nodeData.isolationMethod || '', isolationPoints: Array.isArray(nodeData.isolationPoints) ? nodeData.isolationPoints : [], isolationNode: Array.isArray(nodeData.isolationNode) ? nodeData.isolationNode : [], selectedIsolationNodeId: nodeData.selectedIsolationNodeId || '', lockPerson: nodeData.lockPerson || '', coLockPersons: Array.isArray(nodeData.coLockPersons) ? nodeData.coLockPersons : [], notificationMethods: nodeData.notificationMethods || { sms: false, message: false, email: false, app: false, }, notificationPerson: nodeData.notificationPerson || '', notificationTime: nodeData.notificationTime || '', ...nodeData, // 保留其他可能的字段 }; return { id: nodeId, // 使用uuid或id作为id type: node.type, position: node.position || { x: 0, y: 0 }, data: completeData, }; }); // 还原连接线(source和target需要从uuid映射回id) const importedEdges = data.edges.map((edge: any) => { // 兼容处理:如果source/target是uuid,映射回id;如果是id,直接使用 const sourceId = uuidToIdMap.get(edge.source) || edge.source; const targetId = uuidToIdMap.get(edge.target) || edge.target; return { id: edge.id, source: sourceId, target: targetId, sourceHandle: edge.sourceHandle, targetHandle: edge.targetHandle, type: edge.type || 'straight', animated: false, style: { strokeWidth: 2, stroke: '#94a3b8' }, }; }); // 更新节点和边 setNodes(importedNodes); setEdges(importedEdges); // 缓存每个节点的配置(确保缓存包含完整数据) importedNodes.forEach((node: Node) => { if (node.data) { saveNodeCache(node.id, node.data); } }); // 如果需要更新历史记录(初始化加载时) if (updateHistory) { setHistory([{ nodes: importedNodes, edges: importedEdges }]); setHistoryIndex(0); } return true; } catch (error: any) { console.error('加载 JSON 失败:', error); message.error('JSON 格式错误:' + (error?.message || '解析失败')); return false; } }, [saveNodeCache, setNodes, setEdges, setHistory, setHistoryIndex]); // 进入页面时获取工作流详情 useEffect(() => { const fetchWorkflowDetail = async () => { if (!workflowId) { return; } setLoadingDetail(true); try { const detail = await workflowDesignApi.selectWorkflowDesignById(workflowId); setWorkflowDetail(detail); // 如果有 content,加载到画布上(初始化时更新历史记录) if (detail.content) { loadJsonToCanvas(detail.content, true); } } catch (error: any) { console.error('获取工作流详情失败:', error); message.error(error?.message || '获取工作流详情失败'); } finally { setLoadingDetail(false); } }; fetchWorkflowDetail(); }, [workflowId, loadJsonToCanvas]); // 拖拽处理 const onDragStart = (event: React.DragEvent, nodeType: string) => { event.dataTransfer.setData('application/reactflow', nodeType); event.dataTransfer.effectAllowed = 'move'; }; // 验证连接是否有效(允许所有连接) const isValidConnection = useCallback((connection: Connection) => { // 允许从任意方向连接到任意方向 if (!connection.source || !connection.target) { return false; } // 不允许节点连接到自身 if (connection.source === connection.target) { return false; } return true; }, []); // 修正 Handle 类型的辅助函数 const fixHandleType = (handleId: string | null | undefined, isSource: boolean): string | undefined => { if (!handleId) return undefined; // 如果 sourceHandle 是 target 类型,转换为对应的 source Handle if (isSource && handleId.endsWith('-target')) { const position = handleId.replace('-target', ''); return `${position}-source`; } // 如果 targetHandle 是 source 类型,转换为对应的 target Handle if (!isSource && handleId.endsWith('-source')) { const position = handleId.replace('-source', ''); return `${position}-target`; } return handleId; }; // 连接处理 const onConnect = useCallback( (params: Connection) => { // 确保连接参数包含 sourceHandle 和 targetHandle console.log('连接参数:', params); if (!params.source || !params.target) { console.warn('连接参数无效:', params); return; } if (params.source === params.target) { console.warn('不能连接节点到自身'); return; } // 修正 Handle 类型 let sourceHandle = fixHandleType(params.sourceHandle, true); let targetHandle = fixHandleType(params.targetHandle, false); if (sourceHandle !== params.sourceHandle || targetHandle !== params.targetHandle) { console.log('修正 Handle 类型:', { sourceHandle: `${params.sourceHandle} -> ${sourceHandle}`, targetHandle: `${params.targetHandle} -> ${targetHandle}` }); } setEdges((eds) => { // 检查是否已存在相同的连接(只检查 source 和 target) const existingEdgeIndex = eds.findIndex( (edge) => edge.source === params.source && edge.target === params.target ); if (existingEdgeIndex !== -1) { console.log('连接已存在,更新连接点:', { old: { sourceHandle: eds[existingEdgeIndex].sourceHandle, targetHandle: eds[existingEdgeIndex].targetHandle }, new: { sourceHandle, targetHandle } }); // 更新现有连接的 Handle const updatedEdges = [...eds]; updatedEdges[existingEdgeIndex] = { ...updatedEdges[existingEdgeIndex], sourceHandle, targetHandle, }; // 保存历史 const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ nodes: [...nodes], edges: [...updatedEdges] }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); return updatedEdges; } const edgeId = `edge-${params.source}-${sourceHandle || 'default'}-${params.target}-${targetHandle || 'default'}-${Date.now()}`; const newEdge: Edge = { id: edgeId, source: params.source!, target: params.target!, sourceHandle: sourceHandle || undefined, targetHandle: targetHandle || undefined, animated: false, style: { strokeWidth: 2, stroke: '#94a3b8' }, type: 'straight', }; // 直接添加到数组,不使用 addEdge(因为 addEdge 可能会过滤掉某些连接) const newEdges = [...eds, newEdge]; console.log('新连接已添加:', newEdge); console.log('当前所有连接数量:', newEdges.length); // 保存历史 const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ nodes: [...nodes], edges: [...newEdges] }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); return newEdges; }); }, [setEdges, history, historyIndex, nodes] ); // 更新连接线(拖拽边的端点重新连接) const onEdgeUpdate = useCallback( (oldEdge: Edge, newConnection: Connection) => { console.log('更新连接线:', { oldEdge, newConnection }); if (!newConnection.source || !newConnection.target) { console.warn('新连接参数无效'); return; } if (newConnection.source === newConnection.target) { console.warn('不能连接节点到自身'); return; } // 修正 Handle 类型 let sourceHandle = fixHandleType(newConnection.sourceHandle, true); let targetHandle = fixHandleType(newConnection.targetHandle, false); setEdges((eds) => { // 找到要更新的边 const edgeIndex = eds.findIndex((edge) => edge.id === oldEdge.id); if (edgeIndex === -1) { console.warn('未找到要更新的边'); return eds; } // 检查新连接是否与现有连接冲突(除了当前要更新的边) const conflictingEdge = eds.find( (edge, index) => index !== edgeIndex && edge.source === newConnection.source && edge.target === newConnection.target ); if (conflictingEdge) { console.warn('连接已存在,无法更新'); return eds; } // 更新边的连接点 const updatedEdges = [...eds]; updatedEdges[edgeIndex] = { ...updatedEdges[edgeIndex], source: newConnection.source!, target: newConnection.target!, sourceHandle: sourceHandle || undefined, targetHandle: targetHandle || undefined, }; console.log('连接线已更新:', updatedEdges[edgeIndex]); // 保存历史 const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ nodes: [...nodes], edges: [...updatedEdges] }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); return updatedEdges; }); }, [history, historyIndex, nodes] ); // 节点点击处理 const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => { setSelectedNode(node); setSelectedEdge(null); // 点击节点时取消连线选择 setActiveTabKey('info'); // 切换节点时重置到第一个tab // 清除所有边的选中状态 setEdges((eds) => eds.map((e) => ({ ...e, selected: false, })) ); // 加载节点配置 const nodeData = node.data || {}; const cache = loadNodeCache(node.id); const source = cache || nodeData; const config = nodeConfigs.find(c => c.type === source.type); // 如果是解除隔离节点且已选择了隔离节点,则从隔离节点获取配置 let isolationMethod = source.isolationMethod || ''; let isolationPointsData = source.isolationPoints || []; let lockPerson = source.lockPerson || ''; let coLockPersons = source.coLockPersons || []; let responsible = source.responsible || ''; if (source.type === 'releaseIsolation' && source.selectedIsolationNodeId) { const targetNode = nodes.find(n => n.id === source.selectedIsolationNodeId && n.data?.type === 'isolation'); if (targetNode) { // 优先从缓存中读取,缓存中没有则从 node.data 读取 const targetCache = loadNodeCache(targetNode.id); const targetSource = targetCache || targetNode.data || {}; isolationMethod = targetSource.isolationMethod || ''; isolationPointsData = targetSource.isolationPoints || []; lockPerson = targetSource.lockPerson || ''; coLockPersons = targetSource.coLockPersons || []; responsible = targetSource.responsible || ''; } } setNodeConfig({ nodeName: source.label || config?.label || '', nodeIcon: source.icon || source.type || '', responsible: responsible, remark: source.remark || '', submitForm: source.submitForm || '', isolationMethod: isolationMethod, isolationPoints: isolationPointsData, isolationNode: Array.isArray(source.isolationNode) ? source.isolationNode : (source.isolationNode ? [source.isolationNode] : []), selectedIsolationNodeId: source.selectedIsolationNodeId || '', lockPerson: lockPerson, coLockPersons: coLockPersons, notificationMethods: source.notificationMethods || { sms: false, message: false, email: false, app: false, }, notificationPerson: source.notificationPerson || '', notificationTime: source.notificationTime || '', }); }, [loadNodeCache, nodes, setEdges]); // 画布点击处理(取消选择) const onPaneClick = useCallback(() => { setSelectedNode(null); setSelectedEdge(null); // 清除所有边的选中状态 setEdges((eds) => eds.map((e) => ({ ...e, selected: false, })) ); }, [setEdges]); // 连线点击处理 const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { event.stopPropagation(); console.log('连线被点击:', edge.id, '当前type:', edge.type); setSelectedEdge(edge); setSelectedNode(null); // 点击连线时取消节点选择 // 更新边的选中状态,并确保类型是 straight setEdges((eds) => { const updatedEdges = eds.map((e) => ({ ...e, type: 'straight', // 强制设置为 straight 类型 selected: e.id === edge.id, })); const clickedEdge = updatedEdges.find(e => e.id === edge.id); console.log('更新后的边状态:', clickedEdge?.selected, 'type:', clickedEdge?.type); return updatedEdges; }); }, [setEdges]); // 删除连线 const handleDeleteEdge = useCallback((edgeId: string) => { console.log('删除连线:', edgeId); setEdges((eds) => { const newEdges = eds.filter((edge) => edge.id !== edgeId); console.log('删除后的连线数量:', newEdges.length); // 保存历史 const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ nodes: [...nodes], edges: [...newEdges] }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); return newEdges; }); setSelectedEdge(null); }, [history, historyIndex, nodes]); // 更新全局删除函数 useEffect(() => { globalDeleteEdgeFn = handleDeleteEdge; return () => { globalDeleteEdgeFn = null; }; }, [handleDeleteEdge]); // 确保所有边都使用 'straight' 类型(用于自定义边组件) useEffect(() => { setEdges((eds) => { const needsUpdate = eds.some(e => e.type !== 'straight'); if (needsUpdate) { console.log('统一设置所有边的类型为 straight'); return eds.map(e => ({ ...e, type: 'straight', })); } return eds; }); }, []); // 只在组件挂载时执行一次 // 删除节点 const handleDeleteNode = useCallback((nodeId?: string) => { const targetNodeId = nodeId || selectedNode?.id; if (!targetNodeId) return; Modal.confirm({ title: '确认删除', content: '确定要删除这个节点吗?删除后无法恢复。', okText: '确定删除', okType: 'danger', cancelText: '取消', onOk: () => { setNodes((nds) => { const newNodes = nds.filter((node) => node.id !== targetNodeId); // 同时删除相关的边 setEdges((eds) => { const newEdges = eds.filter( (edge) => edge.source !== targetNodeId && edge.target !== targetNodeId ); // 保存历史 const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ nodes: [...newNodes], edges: [...newEdges] }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); return newEdges; }); // 保存历史 const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ nodes: [...newNodes], edges: [...edges] }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); return newNodes; }); if (selectedNode?.id === targetNodeId) { setSelectedNode(null); } toast.success('节点已删除'); }, }); }, [selectedNode, setNodes, setEdges, history, historyIndex, edges]); // 键盘事件处理(Delete 键删除节点) useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // 如果焦点在输入框、文本域或可编辑元素中,不触发删除节点 const target = event.target as HTMLElement; const isInputElement = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable || target.closest('input') || target.closest('textarea') || target.closest('[contenteditable="true"]'); if (isInputElement) { return; // 在输入框中,不处理删除节点 } if ((event.key === 'Delete' || event.key === 'Backspace') && selectedNode) { event.preventDefault(); handleDeleteNode(); } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [selectedNode, handleDeleteNode]); // 右键菜单处理 const onNodeContextMenu = useCallback( (event: React.MouseEvent, node: Node) => { event.preventDefault(); setSelectedNode(node); }, [] ); // 撤销 const handleUndo = useCallback(() => { if (historyIndex > 0) { const prevState = history[historyIndex - 1]; setNodes(prevState.nodes); setEdges(prevState.edges); setHistoryIndex(historyIndex - 1); } }, [history, historyIndex, setNodes, setEdges]); // 重做 const handleRedo = useCallback(() => { if (historyIndex < history.length - 1) { const nextState = history[historyIndex + 1]; setNodes(nextState.nodes); setEdges(nextState.edges); setHistoryIndex(historyIndex + 1); } }, [history, historyIndex, setNodes, setEdges]); // 拖放处理 const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); const type = event.dataTransfer.getData('application/reactflow'); if (!type || !reactFlowInstance) return; const position = reactFlowInstance.screenToFlowPosition({ x: event.clientX, y: event.clientY, }); const config = nodeConfigs.find(c => c.type === type); const timestamp = Date.now(); const nodeId = `${type}-${timestamp}`; const nodeNumber = nodes.length + 1; const newNode: Node = { id: nodeId, type, position, data: { label: config?.label || type, type, nodeId: String(nodeNumber).padStart(3, '0'), }, }; setNodes((nds) => { const newNodes = nds.concat(newNode); // 保存历史 const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ nodes: [...newNodes], edges: [...edges] }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); return newNodes; }); }, [reactFlowInstance, setNodes, history, historyIndex, edges, nodes.length] ); const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); // 更新节点配置 const updateNodeConfig = useCallback(() => { if (!selectedNode) return; setNodes((nds) => { const updatedNodes = nds.map((node) => { if (node.id === selectedNode.id) { const updatedNode = { ...node, data: { ...node.data, label: nodeConfig.nodeName, type: nodeConfig.nodeIcon || node.data?.type, // 更新节点类型(图标) icon: nodeConfig.nodeIcon, // 保存图标名称 responsible: nodeConfig.responsible, remark: nodeConfig.remark, submitForm: nodeConfig.submitForm, isolationMethod: nodeConfig.isolationMethod, isolationPoints: nodeConfig.isolationPoints, isolationNode: nodeConfig.isolationNode, selectedIsolationNodeId: nodeConfig.selectedIsolationNodeId, lockPerson: nodeConfig.lockPerson, coLockPersons: nodeConfig.coLockPersons, notificationMethods: nodeConfig.notificationMethods, notificationPerson: nodeConfig.notificationPerson, notificationTime: nodeConfig.notificationTime, }, }; // 缓存当前节点配置 saveNodeCache(node.id, updatedNode.data); // 更新 selectedNode 的引用,以便顶部标题能显示最新值 setSelectedNode(updatedNode); return updatedNode; } return node; }); // 保存历史 const newHistory = history.slice(0, historyIndex + 1); newHistory.push({ nodes: [...updatedNodes], edges: [...edges] }); setHistory(newHistory); setHistoryIndex(newHistory.length - 1); return updatedNodes; }); toast.success('节点配置已保存'); }, [selectedNode, nodeConfig, setNodes, setSelectedNode, history, historyIndex, edges]); // 保存流程 const handleSave = async () => { try { // 构建导出数据(与导出JSON逻辑相同) const adjacency: Record = {}; const nodeIdMap = new Map(); nodes.forEach(n => { nodeIdMap.set(n.id, n.id); }); nodes.forEach(n => { const uuid = nodeIdMap.get(n.id) || n.id; adjacency[uuid] = adjacency[uuid] || { incoming: [], outgoing: [] }; }); edges.forEach(e => { const sourceUuid = nodeIdMap.get(e.source) || e.source; const targetUuid = nodeIdMap.get(e.target) || e.target; if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { incoming: [], outgoing: [] }; if (!adjacency[targetUuid]) adjacency[targetUuid] = { incoming: [], outgoing: [] }; adjacency[sourceUuid].outgoing.push(targetUuid); adjacency[targetUuid].incoming.push(sourceUuid); }); const exportData = { generatedAt: new Date().toISOString(), nodeCount: nodes.length, edgeCount: edges.length, adjacency, nodes: nodes.map(n => { const cachedData = loadNodeCache(n.id); const mergedData = cachedData ? { ...n.data, ...cachedData } : n.data; const nodeObj: any = { uuid: n.id, type: n.type, position: n.position, nodeName: mergedData.label || '', nodeIcon: mergedData.icon || mergedData.type || n.type || '', data: mergedData, }; return nodeObj; }), edges: edges.map(e => { return { id: e.id, source: nodeIdMap.get(e.source) || e.source, target: nodeIdMap.get(e.target) || e.target, sourceHandle: e.sourceHandle, targetHandle: e.targetHandle, type: e.type, }; }), }; // 将整个 JSON 作为 content 字段传递 const content = JSON.stringify(exportData, null, 2); // 必须有流程ID才能保存(因为新建时只传了name,设计完成后必须调用更新接口) if (!workflowId) { message.warning('无法保存:缺少流程ID,请先创建流程'); return; } // 从暂存的详情中获取 name 和 description const name = workflowDetail?.name || ''; const description = workflowDetail?.description || ''; // 调用更新接口,传递 id、content、name、description await workflowDesignApi.updateWorkflowDesign({ id: workflowId, content: content, name: name, description: description, }); message.success('流程保存成功'); // 保存成功后返回流程设计列表页面(与返回按钮逻辑相同) navigate('/dashboard'); } catch (error: any) { console.error('保存流程失败:', error); message.error(error?.message || '流程保存失败'); } }; // 返回 const handleBack = () => { // 导航到 dashboard,Dashboard 会从 sessionStorage 读取上次的菜单状态 navigate('/dashboard'); }; // 缩放控制 const handleZoomIn = () => { if (reactFlowInstance) { reactFlowInstance.zoomIn(); } }; const handleZoomOut = () => { if (reactFlowInstance) { reactFlowInstance.zoomOut(); } }; // 导出流程 JSON const handleExportJson = useCallback(() => { // 创建节点ID到UUID的映射(导出时id改为uuid,但值保持不变) const nodeIdMap = new Map(); nodes.forEach(n => { nodeIdMap.set(n.id, n.id); // 值保持不变,只是字段名改为uuid }); // adjacency使用uuid作为key const adjacency: Record = {}; nodes.forEach(n => { const uuid = nodeIdMap.get(n.id) || n.id; adjacency[uuid] = adjacency[uuid] || { incoming: [], outgoing: [] }; }); edges.forEach(e => { const sourceUuid = nodeIdMap.get(e.source) || e.source; const targetUuid = nodeIdMap.get(e.target) || e.target; if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { incoming: [], outgoing: [] }; if (!adjacency[targetUuid]) adjacency[targetUuid] = { incoming: [], outgoing: [] }; adjacency[sourceUuid].outgoing.push(targetUuid); adjacency[targetUuid].incoming.push(sourceUuid); }); const exportData = { generatedAt: new Date().toISOString(), nodeCount: nodes.length, edgeCount: edges.length, adjacency, nodes: nodes.map(n => { // 从缓存中读取最新配置 const cachedData = loadNodeCache(n.id); // 合并缓存数据和节点数据,缓存数据优先级更高 const mergedData = cachedData ? { ...n.data, ...cachedData } : n.data; // 导出时将id字段改为uuid,但值保持不变 // 将 nodeName 和 nodeIcon 从 data 中提取到顶层,方便后端识别,但保留 data 中的原始字段 const nodeObj: any = { uuid: n.id, // 字段名改为uuid,值保持不变 type: n.type, position: n.position, nodeName: mergedData.label || '', // 从 data.label 提取到顶层作为 nodeName nodeIcon: mergedData.icon || mergedData.type || n.type || '', // 从 data.icon 或 data.type 提取到顶层作为 nodeIcon data: mergedData, // 保留完整的 data,包括 label 和 icon }; return nodeObj; }), edges: edges.map(e => { // edges的source和target也需要改为对应的uuid(值相同) return { id: e.id, source: nodeIdMap.get(e.source) || e.source, // 使用映射后的值 target: nodeIdMap.get(e.target) || e.target, // 使用映射后的值 sourceHandle: e.sourceHandle, targetHandle: e.targetHandle, type: e.type, }; }), }; setExportContent(JSON.stringify(exportData, null, 2)); setExportVisible(true); }, [nodes, edges, loadNodeCache]); // 复制JSON到剪贴板 const handleCopyJson = useCallback(async () => { console.log('复制按钮被点击,exportContent长度:', exportContent?.length); if (!exportContent || exportContent.trim() === '') { console.warn('exportContent为空'); message.warning('没有可复制的内容'); return; } try { // 优先使用现代 Clipboard API if (navigator.clipboard && navigator.clipboard.writeText) { console.log('使用 Clipboard API'); await navigator.clipboard.writeText(exportContent); console.log('复制成功'); message.success('复制成功!JSON已复制到剪贴板'); return; } throw new Error('Clipboard API not available'); } catch (error) { console.log('Clipboard API失败,使用降级方案:', error); // 降级方案:使用传统方法 try { const textArea = document.createElement('textarea'); textArea.value = exportContent; textArea.style.position = 'fixed'; textArea.style.top = '0'; textArea.style.left = '0'; textArea.style.width = '2em'; textArea.style.height = '2em'; textArea.style.padding = '0'; textArea.style.border = 'none'; textArea.style.outline = 'none'; textArea.style.boxShadow = 'none'; textArea.style.background = 'transparent'; textArea.style.opacity = '0'; textArea.readOnly = true; textArea.setAttribute('contenteditable', 'true'); document.body.appendChild(textArea); textArea.focus(); textArea.select(); textArea.setSelectionRange(0, exportContent.length); const successful = document.execCommand('copy'); document.body.removeChild(textArea); if (successful) { console.log('传统方法复制成功'); message.success('复制成功!JSON已复制到剪贴板'); } else { console.warn('传统方法复制失败'); message.error('复制失败,请手动复制'); } } catch (err) { console.error('复制失败:', err); message.error('复制失败,请手动复制'); } } }, [exportContent]); // 导入流程 JSON const handleImportJson = useCallback(() => { try { const data = JSON.parse(importJson); if (!data.nodes || !Array.isArray(data.nodes)) { toast.error('JSON 格式错误:缺少 nodes 数组'); return; } if (!data.edges || !Array.isArray(data.edges)) { toast.error('JSON 格式错误:缺少 edges 数组'); return; } // 创建uuid到id的映射(导入时uuid改回id) const uuidToIdMap = new Map(); data.nodes.forEach((node: any) => { // 兼容处理:支持uuid字段或id字段 const nodeId = node.uuid || node.id; uuidToIdMap.set(nodeId, nodeId); // 值保持不变 }); // 还原节点,确保 data 包含所有配置字段 const importedNodes = data.nodes.map((node: any) => { const nodeData = node.data || {}; // 兼容处理:支持uuid字段或id字段 const nodeId = node.uuid || node.id; // 如果顶层有 nodeName 和 nodeIcon,将它们还原到 data 中 const topLevelLabel = node.nodeName || ''; const topLevelIcon = node.nodeIcon || ''; // 确保所有字段都存在,使用默认值填充缺失的字段 const completeData = { label: topLevelLabel || nodeData.label || '', // 优先使用顶层的 nodeName type: nodeData.type || node.type, nodeId: nodeData.nodeId || '', icon: topLevelIcon || nodeData.icon || nodeData.type || node.type, // 优先使用顶层的 nodeIcon responsible: nodeData.responsible || '', remark: nodeData.remark || '', submitForm: nodeData.submitForm || '', isolationMethod: nodeData.isolationMethod || '', isolationPoints: Array.isArray(nodeData.isolationPoints) ? nodeData.isolationPoints : [], isolationNode: Array.isArray(nodeData.isolationNode) ? nodeData.isolationNode : [], selectedIsolationNodeId: nodeData.selectedIsolationNodeId || '', lockPerson: nodeData.lockPerson || '', coLockPersons: Array.isArray(nodeData.coLockPersons) ? nodeData.coLockPersons : [], notificationMethods: nodeData.notificationMethods || { sms: false, message: false, email: false, app: false, }, notificationPerson: nodeData.notificationPerson || '', notificationTime: nodeData.notificationTime || '', ...nodeData, // 保留其他可能的字段 }; return { id: nodeId, // 使用uuid或id作为id type: node.type, position: node.position || { x: 0, y: 0 }, data: completeData, }; }); // 还原连接线(source和target需要从uuid映射回id) const importedEdges = data.edges.map((edge: any) => { // 兼容处理:如果source/target是uuid,映射回id;如果是id,直接使用 const sourceId = uuidToIdMap.get(edge.source) || edge.source; const targetId = uuidToIdMap.get(edge.target) || edge.target; return { id: edge.id, source: sourceId, target: targetId, sourceHandle: edge.sourceHandle, targetHandle: edge.targetHandle, type: edge.type || 'straight', animated: false, style: { strokeWidth: 2, stroke: '#94a3b8' }, }; }); // 更新节点和边 setNodes(importedNodes); setEdges(importedEdges); // 缓存每个节点的配置(确保缓存包含完整数据) importedNodes.forEach((node: Node) => { if (node.data) { saveNodeCache(node.id, node.data); } }); // 如果当前有选中的节点,重新加载其配置 if (selectedNode) { const updatedNode = importedNodes.find(n => n.id === selectedNode.id); if (updatedNode) { const cache = loadNodeCache(updatedNode.id); const source = cache || updatedNode.data || {}; const config = nodeConfigs.find(c => c.type === source.type); setNodeConfig({ nodeName: source.label || config?.label || '', nodeIcon: source.icon || source.type || '', responsible: source.responsible || '', remark: source.remark || '', submitForm: source.submitForm || '', isolationMethod: source.isolationMethod || '', isolationPoints: source.isolationPoints || [], isolationNode: Array.isArray(source.isolationNode) ? source.isolationNode : (source.isolationNode ? [source.isolationNode] : []), selectedIsolationNodeId: source.selectedIsolationNodeId || '', lockPerson: source.lockPerson || '', coLockPersons: source.coLockPersons || [], notificationMethods: source.notificationMethods || { sms: false, message: false, email: false, app: false, }, notificationPerson: source.notificationPerson || '', notificationTime: source.notificationTime || '', }); setSelectedNode(updatedNode); } else { // 如果选中的节点不在导入的数据中,清除选择 setSelectedNode(null); } } // 保存历史 const newHistory = [{ nodes: importedNodes, edges: importedEdges }]; setHistory(newHistory); setHistoryIndex(0); toast.success('流程导入成功'); setImportVisible(false); setImportJson(''); } catch (error: any) { toast.error(`导入失败:${error.message || 'JSON 格式错误'}`); } }, [importJson, setNodes, setEdges, saveNodeCache, loadNodeCache, selectedNode, nodeConfigs, setNodeConfig, setSelectedNode, history, setHistory, setHistoryIndex]); // 获取节点描述 const getNodeDescription = (type: string) => { const descriptions: { [key: string]: string } = { createJob: '该节点为作业创建人员创建作业录入信息开始节点。', confirm: '该节点为作业确认,为"上一节点"操作分配确认模式及确认人员权限', review: '该节点为作业审核,为"上一节点"操作分配审核模式及审核人员权限', inputInfo: '该节点为作业录入提交,可提交信息或图片,主要为"信息确认"', isolation: '该节点为作业隔离类型选择,主要包括盲板,上锁挂牌,拆除等。', releaseIsolation: '该节点为作业隔离类型选择,主要包括盲板,上锁挂牌,拆除等。', returnLock: '该节点为还锁操作,归还钥匙,确认隔离操作完成', complete: '该节点为流程结束点', }; return descriptions[type] || '该节点的功能描述。'; }; return (
{/* 顶部工具栏 */}
流程设计器
{/* 主内容区 */}
{/* 左侧节点面板 */}
{nodeConfigs.map((config) => { const Icon = config.icon; return (
onDragStart(e, config.type)} className={`${config.bgColor} ${config.borderColor} border p-3 rounded-2xl shadow-md flex flex-col items-center justify-center gap-2 cursor-move transition-all hover:shadow-lg`} style={{ borderRadius: '16px', minWidth: '80px', minHeight: '80px', backgroundColor: config.bgColorCustom || undefined }} > {config.label}
); })}
{/* 中间画布 */}
setZoom(viewport.zoom)} connectionMode={ConnectionMode.Loose} defaultEdgeOptions={{ style: { strokeWidth: 2, stroke: '#94a3b8' }, type: 'straight', }} edgesUpdatable={true} edgesFocusable={true} edgeUpdaterRadius={10} > { const config = nodeConfigs.find(c => c.type === node.type); if (config?.color === 'bg-blue-500') return '#3b82f6'; if (config?.color === 'bg-green-500') return '#10b981'; if (config?.color === 'bg-orange-500') return '#f97316'; if (config?.color === 'bg-red-500') return '#ef4444'; if (config?.color === 'bg-yellow-500') return '#eab308'; if (config?.color === 'bg-indigo-500') return '#6366f1'; if (config?.color === 'bg-purple-500') return '#a855f7'; return '#6b7280'; }} maskColor="rgba(0, 0, 0, 0.05)" className="!bg-white !border !border-gray-200 !rounded-lg" />
{/* 右侧属性面板 */}
{selectedNode ? (
{/* 头部 */}

{nodeConfig.nodeName || selectedNode.data?.label || '节点'} 设置

{/* 内容 */}
{/* 节点名称 */}
{/* 描述 - 创建作业、确认、审核、录入信息、隔离方案和解除隔离节点显示在节点名称顶部 */} {(selectedNode.data?.type === 'createJob' || selectedNode.data?.type === 'confirm' || selectedNode.data?.type === 'review' || selectedNode.data?.type === 'inputInfo' || selectedNode.data?.type === 'isolation' || selectedNode.data?.type === 'releaseIsolation' || selectedNode.data?.type === 'returnLock' || selectedNode.data?.type === 'complete') && (
{getNodeDescription(selectedNode.data?.type || '')}
)} {selectedNode.data?.type !== 'createJob' && selectedNode.data?.type !== 'confirm' && (

(默认名称: {nodeConfigs.find(c => c.type === selectedNode.data?.type)?.label || '节点名称'},可根据需求调整)

)} {selectedNode.data?.type === 'createJob' && (

(默认名称: {nodeConfigs.find(c => c.type === selectedNode.data?.type)?.label || '节点名称'},可根据需求调整)

)} setNodeConfig({ ...nodeConfig, nodeName: e.target.value }) } placeholder="请输入节点名称" className="rounded-lg border-gray-200 h-10" />
{/* 底部描述已移至顶部,不再在此显示 */} {/* 显示图标 */}
{availableIcons.slice(0, 25).map((iconItem) => { const IconComponent = iconItem.component; const isSelected = nodeConfig.nodeIcon === iconItem.name || (!nodeConfig.nodeIcon && selectedNode.data?.type && nodeConfigs.find(c => c.type === selectedNode.data?.type)?.icon === IconComponent); return (
{ setNodeConfig({ ...nodeConfig, nodeIcon: iconItem.name }); setIconPickerOpen(false); // 立即更新节点显示 setNodes((nds) => { return nds.map((node) => { if (node.id === selectedNode.id) { return { ...node, data: { ...node.data, icon: iconItem.name, // 保持原有的 type,只更新 icon }, }; } return node; }); }); }} className={` w-12 h-12 rounded-lg border-2 cursor-pointer transition-all flex items-center justify-center hover:border-blue-400 hover:bg-blue-50 ${isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white'} `} title={iconItem.label} style={{ width: '48px', height: '48px' }} >
); })}
} title="选择图标" trigger="click" open={iconPickerOpen} onOpenChange={setIconPickerOpen} placement="right" >
{(() => { // 如果设置了自定义图标,使用自定义图标 let Icon = FileTextOutlined; let config = null; if (nodeConfig.nodeIcon && iconNameMap[nodeConfig.nodeIcon]) { Icon = iconNameMap[nodeConfig.nodeIcon]; // 尝试找到对应的配置 config = nodeConfigs.find(c => c.icon === Icon); } else { const iconType = selectedNode.data?.type; config = nodeConfigs.find(c => c.type === iconType); Icon = config?.icon || FileTextOutlined; } return ( <>
选择图标 ); })()}
{/* 负责人 - 确认节点显示在图标下方 */} {selectedNode.data?.type === 'confirm' && (

对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择。

)} {/* 负责人 - 审核节点显示在图标下方 */} {selectedNode.data?.type === 'review' && (

对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择。

)} {/* 负责人 - 创建作业、隔离方案和解除隔离节点不显示 */} {selectedNode.data?.type !== 'createJob' && selectedNode.data?.type !== 'confirm' && selectedNode.data?.type !== 'review' && selectedNode.data?.type !== 'isolation' && selectedNode.data?.type !== 'releaseIsolation' && (

对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择

)} {/* 备注 - 创建作业、确认、审核、录入信息、隔离方案、解除隔离、还锁和完成节点不显示 */} {selectedNode.data?.type !== 'createJob' && selectedNode.data?.type !== 'confirm' && selectedNode.data?.type !== 'review' && selectedNode.data?.type !== 'inputInfo' && selectedNode.data?.type !== 'isolation' && selectedNode.data?.type !== 'releaseIsolation' && selectedNode.data?.type !== 'returnLock' && selectedNode.data?.type !== 'complete' && (
setNodeConfig({ ...nodeConfig, remark: e.target.value }) } placeholder="请输入备注" rows={3} className="rounded-lg border-gray-200" />
)}
), }, { key: 'form', label: '提交表单', children: (
{/* 隔离/方案 和 解除隔离 节点特有的字段 */} {(selectedNode.data?.type === 'isolation' || selectedNode.data?.type === 'releaseIsolation') && ( <> {/* 解除隔离节点:选择隔离节点 */} {selectedNode.data?.type === 'releaseIsolation' && (
)} {/* 隔离方式 - 隔离方案节点可编辑,解除隔离节点只读(根据选择的隔离节点自动填充) */}
{/* 隔离点选择(可多选)- 隔离方案节点显示,解除隔离只读展示 */} {(selectedNode.data?.type === 'isolation' || selectedNode.data?.type === 'releaseIsolation') && (
)} {/* 盲板和拆除:显示负责人 */} {(nodeConfig.isolationMethod === 'blindPlate' || nodeConfig.isolationMethod === 'remove') && (

对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择。

)} {/* 上锁挂牌:显示上锁人和共锁人 */} {nodeConfig.isolationMethod === 'lockTag' && ( <>
)} )}
), }, { key: 'notification', label: '通知消息', children: (
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, sms: e.target.checked, }, }) } > 短信
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, message: e.target.checked, }, }) } > 站内信
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, email: e.target.checked, }, }) } > 邮件
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, app: e.target.checked, }, }) } > APP通知
), }, // 通知消息tab - 已隐藏但保留代码 { key: 'notification', label: '通知消息', children: (
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, sms: e.target.checked, }, }) } > 短信
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, message: e.target.checked, }, }) } > 站内信
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, email: e.target.checked, }, }) } > 邮件
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, app: e.target.checked, }, }) } > APP通知
), }, ].filter(item => item.key !== 'notification')} />
) : (

请点击画布中的节点查看和编辑属性

)}
setExportVisible(false)} onOk={() => setExportVisible(false)} width={800} okText="关闭" cancelText="取消" style={{ top: 20 }} styles={{ body: { maxHeight: 'calc(90vh - 120px)', overflowY: 'auto', overflowX: 'hidden', padding: 0 } }} >
{/* 固定在顶部的复制按钮 */}
{/* JSON内容区域 */}
                {exportContent}
              
{ setImportVisible(false); setImportJson(''); }} onOk={handleImportJson} width={800} okText="导入" cancelText="取消" >

请粘贴流程 JSON 数据,导入后将替换当前画布内容

setImportJson(e.target.value)} placeholder="请粘贴 JSON 数据..." rows={15} className="font-mono text-xs" />
); }