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, getBezierPath, getSmoothStepPath, EdgeTypes, type NodeChange, } from 'reactflow'; import 'reactflow/dist/style.css'; import { SaveOutlined, ArrowLeftOutlined, UndoOutlined, RedoOutlined, ToolOutlined, CheckCircleOutlined, FileTextOutlined, EditOutlined, SafetyOutlined, UnlockOutlined, LockOutlined, CheckSquareOutlined, CloseOutlined, MenuFoldOutlined, MenuUnfoldOutlined, 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, Card, Alert, InputNumber, Radio, DatePicker, Form as AntdForm, Cascader, Upload, Switch, Tooltip } 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, getForm, FormVO } from '../api/bpm/form'; import { setConfAndFields2, FormCreateData } from '../utils/formCreate'; import { dictDataApi, DictDataVO } from '../api/DictData'; // 节点配置 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; let iconImagePath: string | null = null; // 检查是否是图片文件名(如 "1000.png") if (data.icon && /^\d+\.png$/.test(data.icon)) { // 是图片文件名,获取对应的图片路径 iconImagePath = getIconPathByFileName(data.icon); // 尝试找到对应的配置(保持颜色等样式) config = nodeConfigs.find(c => c.type === data.type) || nodeConfigs[0]; } else 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'); // 处理节点名称:如果超过6个字符则换行 const formatNodeLabel = (text: string): string[] => { if (!text) return []; if (text.length <= 6) return [text]; // 如果超过6个字符,按6个字符分割 const lines: string[] = []; for (let i = 0; i < text.length; i += 6) { lines.push(text.slice(i, i + 6)); } return lines; }; const nodeLabel = data.label || config?.label || ''; const labelLines = formatNodeLabel(nodeLabel); return (
{/* 连接点 - 四个方向,每个方向都有唯一的 source 和 target Handle */} {/* 垂直布局:顶部图标、中间名称、底部ID */}
{/* 顶部:图标 */}
{iconImagePath ? ( {data.icon { // 如果图片加载失败,回退到默认图标 console.error('节点图标加载失败:', iconImagePath); (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : ( )}
{/* 中间:节点名称 */}
{labelLines.length > 1 ? ( labelLines.map((line, index) => ( {line} )) ) : ( {nodeLabel} )}
{/* 底部: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; // 全局变量存储 edges(用于边组件获取 type) let globalEdges: Edge[] = []; // 自定义边组件 - 定义在组件外部以确保稳定引用 function CustomEdgeWithDelete({ id, sourceX, sourceY, targetX, targetY, selected, markerStart, style, type: propType, }: any) { // 从全局 edges 中查找对应的边来获取 type 和 fixedLength const edge = globalEdges.find(e => e.id === id); const edgeType = edge?.type || propType || 'straight'; const fixedLength = (edge as any)?.data?.fixedLength || (edge as any)?.fixedLength; // 计算实际距离 const dx = targetX - sourceX; const dy = targetY - sourceY; const actualDistance = Math.sqrt(dx * dx + dy * dy); // 如果设置了 fixedLength 且实际距离大于 fixedLength,则调整起点和终点 let adjustedSourceX = sourceX; let adjustedSourceY = sourceY; let adjustedTargetX = targetX; let adjustedTargetY = targetY; if (fixedLength && actualDistance > fixedLength && actualDistance > 0.001) { // 计算单位方向向量 const unitX = dx / actualDistance; const unitY = dy / actualDistance; // 计算需要缩短的距离(从两端各缩短一半) const shortenDistance = (actualDistance - fixedLength) / 2; // 调整起点(向目标方向移动) adjustedSourceX = sourceX + unitX * shortenDistance; adjustedSourceY = sourceY + unitY * shortenDistance; // 调整终点(向源点方向移动) adjustedTargetX = targetX - unitX * shortenDistance; adjustedTargetY = targetY - unitY * shortenDistance; } // 根据边的类型选择路径生成函数 let edgePath: string; let labelX: number; let labelY: number; if (edgeType === 'smoothstep') { // 使用 smoothstep 路径(平滑步进曲线,可以拐弯) // 调整 borderRadius 参数使曲线更平滑 [edgePath, labelX, labelY] = getSmoothStepPath({ sourceX: adjustedSourceX, sourceY: adjustedSourceY, targetX: adjustedTargetX, targetY: adjustedTargetY, borderRadius: 15, // 圆角半径,使曲线更平滑 }); } else { // 使用直线路径 [edgePath, labelX, labelY] = getStraightPath({ sourceX: adjustedSourceX, sourceY: adjustedSourceY, targetX: adjustedTargetX, targetY: adjustedTargetY, }); } console.log('边组件渲染,ID:', id, 'propType:', propType, 'edgeType:', edgeType, 'edge对象:', edge, 'sourceX:', sourceX, 'sourceY:', sourceY, 'targetX:', targetX, 'targetY:', targetY, 'selected:', selected); return ( <> {selected && (
)} ); } // 自定义边类型映射 - 定义在组件外部 const edgeTypes: EdgeTypes = { straight: CustomEdgeWithDelete, default: CustomEdgeWithDelete, smoothstep: CustomEdgeWithDelete, }; // 节点类型到图标分类的映射 const getIconCategoryByNodeType = (nodeType: string): string => { const typeMap: Record = { 'createJob': '开始', 'confirm': '确认', 'review': '审核', 'inputInfo': '录入', 'isolation': '能量隔离', 'releaseIsolation': '解除隔离', 'returnLock': '结束', 'complete': '结束', }; return typeMap[nodeType] || '开始'; }; // 图标分类配置(分类名称 -> 图标文件列表) const iconCategories: Record = { '审核': { start: 1000, end: 1011 }, '开始': { start: 2000, end: 2016 }, '录入': { start: 3000, end: 3028 }, '确认': { start: 4000, end: 4024 }, '结束': { start: 5000, end: 5018 }, '能量隔离': { start: 6000, end: 6027 }, '解除隔离': { start: 7000, end: 7021 }, }; // 使用 import.meta.glob 动态导入所有图标 const iconModules = import.meta.glob('../assets/节点图标/**/*.png', { eager: true, as: 'url' }); // 调试:打印加载的图标模块信息 const allIconKeys = Object.keys(iconModules); console.log('ProcessDesigner: 图标模块加载情况'); console.log('总数量:', allIconKeys.length); if (allIconKeys.length > 0) { console.log('前5个路径示例:', allIconKeys.slice(0, 5)); // 打印一个完整的路径示例 const firstKey = allIconKeys[0]; console.log('第一个路径完整信息:', { key: firstKey, value: iconModules[firstKey], normalized: firstKey.replace(/\\/g, '/') }); } else { console.error('ProcessDesigner: 未加载到任何图标模块!'); console.error('请检查路径: ../assets/节点图标/**/*.png'); console.error('提示: 如果路径正确,可能需要重启开发服务器'); } // 生成图标路径列表(返回文件名和路径的映射) const generateIconPaths = (category: string): Array<{ id: string; fileName: string; path: string }> => { const config = iconCategories[category]; if (!config) { console.warn(`ProcessDesigner: 分类 ${category} 没有配置`); return []; } const paths: Array<{ id: string; fileName: string; path: string }> = []; // 如果 import.meta.glob 加载成功,使用它 const allKeys = Object.keys(iconModules); if (allKeys.length > 0) { // 使用 import.meta.glob 的结果 for (let i = config.start; i <= config.end; i++) { const fileName = `${i}.png`; const matchingKey = allKeys.find(k => { const normalizedKey = k.replace(/\\/g, '/').toLowerCase(); const normalizedCategory = category.toLowerCase(); const normalizedFileName = fileName.toLowerCase(); return normalizedKey.includes(normalizedCategory) && normalizedKey.endsWith(normalizedFileName); }); if (matchingKey) { const iconPath = iconModules[matchingKey] as string; paths.push({ id: `${category}_${i}`, fileName: fileName, path: iconPath }); } } } else { // Fallback: 使用 new URL 动态构建路径(适用于开发环境) console.warn('ProcessDesigner: import.meta.glob 未加载成功,使用 fallback 方式'); for (let i = config.start; i <= config.end; i++) { try { // 使用 new URL 构建路径 const fileName = `${i}.png`; const iconPath = new URL(`../assets/节点图标/${category}/${fileName}`, import.meta.url).href; paths.push({ id: `${category}_${i}`, fileName: fileName, path: iconPath }); } catch (e) { console.warn(`ProcessDesigner: 无法构建图标路径 ${category}/${i}.png:`, e); } } } return paths; }; // 根据文件名获取图标路径(用于节点显示) const getIconPathByFileName = (fileName: string | undefined): string | null => { if (!fileName) return null; // 如果已经是完整路径,尝试提取文件名 let actualFileName = fileName; if (fileName.includes('/') || fileName.includes('\\')) { // 从路径中提取文件名 const pathParts = fileName.replace(/\\/g, '/').split('/'); actualFileName = pathParts[pathParts.length - 1]; } // 从文件名中提取数字和分类 const match = actualFileName.match(/^(\d+)\.png$/); if (!match) return null; const iconNumber = parseInt(match[1], 10); // 根据数字范围判断分类 let category = ''; if (iconNumber >= 1000 && iconNumber <= 1011) category = '审核'; else if (iconNumber >= 2000 && iconNumber <= 2016) category = '开始'; else if (iconNumber >= 3000 && iconNumber <= 3028) category = '录入'; else if (iconNumber >= 4000 && iconNumber <= 4024) category = '确认'; else if (iconNumber >= 5000 && iconNumber <= 5018) category = '结束'; else if (iconNumber >= 6000 && iconNumber <= 6027) category = '能量隔离'; else if (iconNumber >= 7000 && iconNumber <= 7021) category = '解除隔离'; if (!category) return null; // 查找对应的路径 const allKeys = Object.keys(iconModules); const matchingKey = allKeys.find(k => { const normalizedKey = k.replace(/\\/g, '/').toLowerCase(); return normalizedKey.includes(category.toLowerCase()) && normalizedKey.endsWith(actualFileName.toLowerCase()); }); if (matchingKey) { return iconModules[matchingKey] as string; } return null; }; // 从图标路径或文件名中提取文件名(用于保存JSON) const extractIconFileName = (icon: string | undefined): string | undefined => { if (!icon) return undefined; // 如果已经是文件名格式(如 "1000.png"),直接返回 if (/^\d+\.png$/.test(icon)) { return icon; } // 如果是路径,提取文件名 if (icon.includes('/') || icon.includes('\\')) { const pathParts = icon.replace(/\\/g, '/').split('/'); const fileName = pathParts[pathParts.length - 1]; // 如果提取的文件名符合格式,返回它 if (/^\d+\.png$/.test(fileName)) { return fileName; } } // 否则返回原值(可能是其他格式的图标) return icon; }; export default function ProcessDesigner() { const navigate = useNavigate(); const location = useLocation(); // 返回流程模板菜单的辅助函数 const backToProcessTemplateMenu = () => { // 默认返回:隔离作业 -> 流程模板 let menuInfo = { menu: 'isolationWork', subMenu: 'processTemplate' }; try { const source = sessionStorage.getItem('processDesignerSource'); if (source) { const parsed = JSON.parse(source); if (parsed?.menu && parsed?.subMenu) { menuInfo = { menu: parsed.menu, subMenu: parsed.subMenu }; } sessionStorage.removeItem('processDesignerSource'); } } catch (e) { console.error('解析 processDesignerSource 失败:', e); } sessionStorage.setItem('navigateToMenu', JSON.stringify(menuInfo)); sessionStorage.setItem('lastActiveMenu', JSON.stringify(menuInfo)); navigate('/dashboard'); }; const [nodes, setNodes, onNodesChangeBase] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); // 保存初始状态,用于检测是否有未保存的更改 const initialNodesRef = useRef([]); const initialEdgesRef = useRef([]); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // 是否正处于「从服务器/缓存加载或恢复」阶段,此期间不把 onNodesChange/onEdgesChange 视为用户修改 const isLoadingFromServerOrRestoreRef = useRef(false); // 从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 [rightPanelCollapsed, setRightPanelCollapsed] = useState(false); 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 [formPreviewVisible, setFormPreviewVisible] = useState(false); const [formPreviewData, setFormPreviewData] = useState(null); const [formPreviewLoading, setFormPreviewLoading] = useState(false); const [formPreviewDetailData, setFormPreviewDetailData] = useState({ rule: [], option: {} }); const formPreviewForm = AntdForm.useForm()[0]; const defaultFormConfig = { name: '', labelPosition: 'right', formSize: 'middle', labelSuffix: '', labelWidth: 100, hideRequiredMark: false, showValidationError: true, inlineValidation: false, showSubmitButton: false, showResetButton: 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 [isolationTypeDictList, setIsolationTypeDictList] = 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 } }, []); // 流程设计JSON缓存相关函数 const getCacheKey = useCallback((id: number | null) => { return id ? `process_designer_cache_${id}` : null; }, []); // 保存流程设计JSON到缓存 const saveWorkflowCache = useCallback((id: number | null, nodes: Node[], edges: Edge[]) => { if (!id) return; try { // 构建导出数据(与handleSave中的逻辑相同) 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] || { parentUuid: [], childrenUuid: [] }; }); edges.forEach(e => { const sourceUuid = nodeIdMap.get(e.source) || e.source; const targetUuid = nodeIdMap.get(e.target) || e.target; if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { parentUuid: [], childrenUuid: [] }; if (!adjacency[targetUuid]) adjacency[targetUuid] = { parentUuid: [], childrenUuid: [] }; adjacency[sourceUuid].parentUuid.push(targetUuid); adjacency[targetUuid].childrenUuid.push(sourceUuid); }); const exportData = { nodeCount: nodes.length, edgeCount: edges.length, adjacency, nodes: nodes.map(n => { const cachedData = loadNodeCache(n.id); const mergedData = cachedData ? { ...n.data, ...cachedData } : n.data; // 将 responsible 转换为 workerUserId,并确保值是字符串 // 同时提取四个模板代码字段到顶层 const { responsible, smsTemplateCode, messageTemplateCode, emailTemplateCode, appTemplateCode, ...restData } = mergedData; const processedData = { ...restData, workerUserId: responsible !== undefined && responsible !== null && responsible !== '' ? String(responsible) : '', }; const nodeObj: any = { uuid: n.id, type: n.type, position: n.position, nodeName: processedData.label || '', nodeIcon: processedData.icon || processedData.type || n.type || '', smsTemplateCode: smsTemplateCode || 'false', messageTemplateCode: messageTemplateCode || 'false', emailTemplateCode: emailTemplateCode || 'false', appTemplateCode: appTemplateCode || 'false', data: processedData, }; 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 || 'straight', }; }), }; const content = JSON.stringify(exportData, null, 2); const cacheKey = getCacheKey(id); if (cacheKey) { localStorage.setItem(cacheKey, content); console.log('流程设计已保存到缓存:', cacheKey); } } catch (error) { console.error('保存流程设计缓存失败:', error); } }, [loadNodeCache, getCacheKey]); // 读取流程设计JSON缓存 const loadWorkflowCache = useCallback((id: number | null): string | null => { if (!id) return null; const cacheKey = getCacheKey(id); if (!cacheKey) return null; try { return localStorage.getItem(cacheKey); } catch { return null; } }, [getCacheKey]); // 清除流程设计JSON缓存 const clearWorkflowCache = useCallback((id: number | null) => { if (!id) return; const cacheKey = getCacheKey(id); if (cacheKey) { localStorage.removeItem(cacheKey); console.log('流程设计缓存已清除:', cacheKey); } }, [getCacheKey]); // 清除当前流程的所有节点缓存 const clearCurrentWorkflowNodeCache = useCallback(() => { if (!nodes || nodes.length === 0) return; try { nodes.forEach((node) => { const nodeCacheKey = `${cachePrefix}${node.id}`; sessionStorage.removeItem(nodeCacheKey); }); console.log('当前流程的节点缓存已清除'); } catch (error) { console.error('清除节点缓存失败:', error); } }, [nodes]); // 比较两个 JSON 字符串内容是否相同(忽略格式差异) const compareJsonContent = useCallback((json1: string | null, json2: string | null): boolean => { if (!json1 || !json2) return false; try { const obj1 = JSON.parse(json1); const obj2 = JSON.parse(json2); // 使用 JSON.stringify 比较,确保顺序一致 return JSON.stringify(obj1) === JSON.stringify(obj2); } catch { // 如果解析失败,直接比较字符串 return json1.trim() === json2.trim(); } }, []); // 比较当前状态和初始状态,检测是否有未保存的更改 const checkUnsavedChanges = useCallback(() => { // 比较节点数量 if (nodes.length !== initialNodesRef.current.length) { return true; } // 比较边数量 if (edges.length !== initialEdgesRef.current.length) { return true; } // 创建初始节点的映射,方便通过ID查找 const initialNodesMap = new Map(initialNodesRef.current.map(n => [n.id, n])); const initialEdgesMap = new Map(initialEdgesRef.current.map(e => [e.id, e])); // 深度比较节点(包括位置、数据等) const nodesChanged = nodes.some((node) => { const initialNode = initialNodesMap.get(node.id); if (!initialNode) return true; // 新节点 // 比较基本属性 if (node.type !== initialNode.type || node.position.x !== initialNode.position.x || node.position.y !== initialNode.position.y) { return true; } // 比较节点数据(包括缓存数据) const cachedData = loadNodeCache(node.id); const mergedData = cachedData ? { ...node.data, ...cachedData } : node.data; // 对于初始节点,我们只比较初始数据,不考虑初始时的缓存 const initialMergedData = initialNode.data; return JSON.stringify(mergedData) !== JSON.stringify(initialMergedData); }); if (nodesChanged) { return true; } // 检查是否有节点被删除 const hasDeletedNode = initialNodesRef.current.some(initialNode => !nodes.find(n => n.id === initialNode.id) ); if (hasDeletedNode) { return true; } // 深度比较边 const edgesChanged = edges.some((edge) => { const initialEdge = initialEdgesMap.get(edge.id); if (!initialEdge) return true; // 新边 return edge.source !== initialEdge.source || edge.target !== initialEdge.target || edge.sourceHandle !== initialEdge.sourceHandle || edge.targetHandle !== initialEdge.targetHandle || edge.type !== initialEdge.type; }); if (edgesChanged) { return true; } // 检查是否有边被删除 const hasDeletedEdge = initialEdgesRef.current.some(initialEdge => !edges.find(e => e.id === initialEdge.id) ); return hasDeletedEdge; }, [nodes, edges, loadNodeCache]); // 调试:监听 edges 变化 useEffect(() => { console.log('Edges 状态更新:', edges.length, edges); }, [edges]); // 仅在用户真正修改过(hasUnsavedChanges)时写入缓存,供异常退出后恢复使用 useEffect(() => { if (workflowId && (nodes.length > 0 || edges.length > 0) && hasUnsavedChanges) { saveWorkflowCache(workflowId, nodes, edges); } }, [nodes, edges, workflowId, hasUnsavedChanges, saveWorkflowCache]); // 获取隔离方式字典数据 const getIsolationMethodDictList = async () => { try { const response = await dictDataApi.getDictDataPage({ pageNo: 1, pageSize: -1, dictType: 'isolation_method', }); const data = (response as any)?.data || response; const list = data?.list || []; setIsolationTypeDictList(list); } catch (error: any) { console.error('获取隔离方式字典失败:', error); setIsolationTypeDictList([]); } }; // 加载角色用户列表和隔离点列表 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, status: 0 }); // 只获取开启状态(status=0)的表单 setFormList(res.list || []); } catch (error) { console.error('加载表单列表失败:', error); } }; loadRoleUsers(); loadIsolationPoints(); loadFormList(); getIsolationMethodDictList(); }, []); // 节点配置状态 const [nodeConfig, setNodeConfig] = useState({ nodeName: '', nodeIcon: '', responsible: '', remark: '', formId: '', isolationType: '', // 隔离方式 isolationPoints: [] as string[], // 隔离点选择(多选) isolationNode: [] as string[], // 隔离节点(多选) isolationNodeUuid: '', // 选择的隔离/方案节点ID(用于解除隔离节点) lockPerson: '', // 上锁人 coLockPersons: [] as string[], // 共锁人(多选) notificationMethods: { sms: false, message: false, email: false, app: false, }, notificationPerson: '', notificationTime: '', smsTemplateCode: 'false', messageTemplateCode: 'false', emailTemplateCode: 'false', appTemplateCode: 'false', }); // 实时缓存并更新节点配置,避免切换节点后丢失未保存的输入 // 若当前为隔离/方案节点,将修改内容自动同步到所有已关联的解除隔离节点,避免设计人员忘记手动维护 useEffect(() => { if (selectedNode) { const { isolationMethod, ...restData } = selectedNode.data || {}; // 将 responsible 同时保存为 workerUserId,确保数据一致性 const responsibleValue = nodeConfig.responsible || ''; const updatedData = { ...restData, label: nodeConfig.nodeName, icon: nodeConfig.nodeIcon, responsible: responsibleValue, workerUserId: responsibleValue, // 同时保存 workerUserId 字段 remark: nodeConfig.remark, formId: nodeConfig.formId, isolationType: nodeConfig.isolationType, isolationPoints: nodeConfig.isolationPoints, isolationNode: nodeConfig.isolationNode, isolationNodeUuid: nodeConfig.isolationNodeUuid, lockPerson: nodeConfig.lockPerson, coLockPersons: nodeConfig.coLockPersons, notificationMethods: nodeConfig.notificationMethods, notificationPerson: nodeConfig.notificationPerson, notificationTime: nodeConfig.notificationTime, smsTemplateCode: nodeConfig.smsTemplateCode, messageTemplateCode: nodeConfig.messageTemplateCode, emailTemplateCode: nodeConfig.emailTemplateCode, appTemplateCode: nodeConfig.appTemplateCode, nodeName: nodeConfig.nodeName, nodeIcon: nodeConfig.nodeIcon, }; // 缓存配置 saveNodeCache(selectedNode.id, updatedData); const isIsolationNode = selectedNode.data?.type === 'isolation'; const isolationSyncPayload = isIsolationNode ? { isolationType: nodeConfig.isolationType, isolationPoints: nodeConfig.isolationPoints, lockPerson: nodeConfig.lockPerson, coLockPersons: nodeConfig.coLockPersons, responsible: responsibleValue, workerUserId: responsibleValue, } : null; // 实时更新节点显示;若当前为隔离/方案节点,则把修改内容同步到所有已关联的解除隔离节点 setNodes((nds) => nds.map((node) => { if (node.id === selectedNode.id) { return { ...node, data: updatedData }; } if ( isolationSyncPayload && node.data?.type === 'releaseIsolation' && String(node.data?.isolationNodeUuid) === String(selectedNode.id) ) { const releaseData = { ...node.data, ...isolationSyncPayload, }; saveNodeCache(node.id, releaseData); return { ...node, data: releaseData }; } return 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) => { if (updateHistory) { isLoadingFromServerOrRestoreRef.current = true; } 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 || ''; // 从顶层读取四个模板代码字段(与 uuid 同级) const topLevelSmsTemplateCode = node.smsTemplateCode; const topLevelMessageTemplateCode = node.messageTemplateCode; const topLevelEmailTemplateCode = node.emailTemplateCode; const topLevelAppTemplateCode = node.appTemplateCode; // 兼容处理:优先使用 workerUserId,如果没有则使用 responsible(向后兼容),并确保值是字符串 const workerUserIdValue = nodeData.workerUserId !== undefined && nodeData.workerUserId !== null && nodeData.workerUserId !== '' ? String(nodeData.workerUserId) : (nodeData.responsible !== undefined && nodeData.responsible !== null && nodeData.responsible !== '' ? String(nodeData.responsible) : ''); // 确保所有字段都存在,使用默认值填充缺失的字段 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: workerUserIdValue, // 内部仍使用 responsible 字段名,但值来自 workerUserId 或 responsible workerUserId: workerUserIdValue, // 同时保存 workerUserId 字段,确保数据一致性 remark: nodeData.remark || '', formId: nodeData.formId || nodeData.submitForm ? String(nodeData.formId || nodeData.submitForm) : '', // 兼容旧的 submitForm 字段,确保是字符串类型 isolationType: nodeData.isolationType || '', isolationPoints: Array.isArray(nodeData.isolationPoints) ? nodeData.isolationPoints : [], isolationNode: Array.isArray(nodeData.isolationNode) ? nodeData.isolationNode : [], isolationNodeUuid: nodeData.isolationNodeUuid || '', 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 || '', smsTemplateCode: topLevelSmsTemplateCode !== undefined ? topLevelSmsTemplateCode : (nodeData.smsTemplateCode || 'false'), messageTemplateCode: topLevelMessageTemplateCode !== undefined ? topLevelMessageTemplateCode : (nodeData.messageTemplateCode || 'false'), emailTemplateCode: topLevelEmailTemplateCode !== undefined ? topLevelEmailTemplateCode : (nodeData.emailTemplateCode || 'false'), appTemplateCode: topLevelAppTemplateCode !== undefined ? topLevelAppTemplateCode : (nodeData.appTemplateCode || 'false'), ...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: '#000000' }, markerStart: { type: 'arrowclosed', color: '#000000', }, data: { fixedLength: edge.data?.fixedLength || 150, // 保留导入的 fixedLength 或设置为默认值 150 }, }; }); // 更新节点和边 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); // 更新初始状态,用于检测未保存的更改 initialNodesRef.current = JSON.parse(JSON.stringify(importedNodes)); initialEdgesRef.current = JSON.parse(JSON.stringify(importedEdges)); setHasUnsavedChanges(false); // 短时内不把后续的 onNodesChange/onEdgesChange 视为用户修改,避免保存后再次进入仍弹「未保存」框 setTimeout(() => { isLoadingFromServerOrRestoreRef.current = false; }, 200); } return true; } catch (error: any) { if (updateHistory) { isLoadingFromServerOrRestoreRef.current = false; } 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); // 检查是否有缓存 const cachedContent = loadWorkflowCache(workflowId); const hasApiContent = detail.content && detail.content.trim().length > 0; const hasCacheContent = cachedContent && cachedContent.trim().length > 0; // 如果接口有内容且缓存也有内容,需要比较内容是否相同 if (hasApiContent && hasCacheContent) { // 比较缓存内容和接口内容是否相同 const isContentSame = compareJsonContent(cachedContent, detail.content); if (isContentSame) { // 内容相同,说明用户没有修改,直接使用接口内容,不提示 loadJsonToCanvas(detail.content, true); } else { // 内容不同,说明用户有修改,弹出确认框 Modal.confirm({ title: '检测到未保存的设计', content: '检测到您有未保存的设计稿(可能为异常退出),需要恢复继续设计吗?', okText: '恢复', cancelText: '不恢复', onOk: () => { // 使用缓存内容恢复;恢复后视为「未保存」以便点击返回时提示保存或放弃 loadJsonToCanvas(cachedContent, true); setHasUnsavedChanges(true); }, onCancel: () => { // 使用接口内容,并清除缓存避免下次再提示 clearWorkflowCache(workflowId); loadJsonToCanvas(detail.content, true); }, }); } } else if (hasCacheContent) { // 只有缓存,直接使用缓存 loadJsonToCanvas(cachedContent, true); } else if (hasApiContent) { // 只有接口内容,使用接口内容 loadJsonToCanvas(detail.content, true); } } catch (error: any) { console.error('获取工作流详情失败:', error); message.error(error?.message || '获取工作流详情失败'); } finally { setLoadingDetail(false); } }; fetchWorkflowDetail(); }, [workflowId, loadJsonToCanvas, loadWorkflowCache, compareJsonContent, clearWorkflowCache]); // 拖拽处理 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, type: 'straight', // 统一使用直线 markerStart: { type: 'arrowclosed', color: '#000000', }, data: { fixedLength: 150, // 设置连接线固定长度为150 }, }; // 保存历史 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: '#000000' }, type: 'straight', // 统一使用直线 markerStart: { type: 'arrowclosed', color: '#000000', }, data: { fixedLength: 150, // 设置连接线固定长度为150 }, }; console.log('创建新边,类型:', newEdge.type, '边对象:', newEdge); // 直接添加到数组,不使用 addEdge(因为 addEdge 可能会过滤掉某些连接) const newEdges = [...eds, newEdge]; console.log('新连接已添加:', newEdge); console.log('新边的类型:', newEdge.type); 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; }); setHasUnsavedChanges(true); }, [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]; const existingEdge = updatedEdges[edgeIndex]; updatedEdges[edgeIndex] = { ...existingEdge, source: newConnection.source!, target: newConnection.target!, sourceHandle: sourceHandle || undefined, targetHandle: targetHandle || undefined, data: { ...(existingEdge.data || {}), fixedLength: (existingEdge.data as any)?.fixedLength || 150, // 保留或设置 fixedLength }, }; 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; }); setHasUnsavedChanges(true); }, [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 isolationType = source.isolationType || ''; let isolationPointsData = source.isolationPoints || []; let lockPerson = source.lockPerson || ''; let coLockPersons = source.coLockPersons || []; // 优先使用 workerUserId,如果没有则使用 responsible(向后兼容),并确保值是字符串 let responsible = (source.workerUserId !== undefined && source.workerUserId !== null && source.workerUserId !== '') ? String(source.workerUserId) : (source.responsible !== undefined && source.responsible !== null && source.responsible !== '') ? String(source.responsible) : ''; if (source.type === 'releaseIsolation' && source.isolationNodeUuid) { const targetNode = nodes.find(n => n.id === source.isolationNodeUuid && n.data?.type === 'isolation'); if (targetNode) { // 优先从缓存中读取,缓存中没有则从 node.data 读取 const targetCache = loadNodeCache(targetNode.id); const targetSource = targetCache || targetNode.data || {}; isolationType = targetSource.isolationType || ''; isolationPointsData = targetSource.isolationPoints || []; lockPerson = targetSource.lockPerson || ''; coLockPersons = targetSource.coLockPersons || []; // 优先使用 workerUserId,如果没有则使用 responsible(向后兼容),并确保值是字符串 responsible = (targetSource.workerUserId !== undefined && targetSource.workerUserId !== null && targetSource.workerUserId !== '') ? String(targetSource.workerUserId) : (targetSource.responsible !== undefined && targetSource.responsible !== null && targetSource.responsible !== '') ? String(targetSource.responsible) : ''; } } setNodeConfig({ nodeName: source.label || config?.label || '', nodeIcon: source.icon || source.type || '', responsible: responsible, remark: source.remark || '', formId: source.formId || source.submitForm ? String(source.formId || source.submitForm) : '', // 兼容旧的 submitForm 字段,确保是字符串类型 isolationType: isolationType, isolationPoints: isolationPointsData, isolationNode: Array.isArray(source.isolationNode) ? source.isolationNode : (source.isolationNode ? [source.isolationNode] : []), isolationNodeUuid: source.isolationNodeUuid || '', lockPerson: lockPerson, coLockPersons: coLockPersons, notificationMethods: source.notificationMethods || { sms: false, message: false, email: false, app: false, }, notificationPerson: source.notificationPerson || '', notificationTime: source.notificationTime || '', smsTemplateCode: source.smsTemplateCode || 'false', messageTemplateCode: source.messageTemplateCode || 'false', emailTemplateCode: source.emailTemplateCode || 'false', appTemplateCode: source.appTemplateCode || 'false', }); // 如果是创建作业节点,且当前tab是"提交表单",自动切换到"节点信息"tab if (node.data?.type === 'createJob' && activeTabKey === 'form') { setActiveTabKey('info'); } }, [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: edge.type || '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); setHasUnsavedChanges(true); }, [history, historyIndex, nodes]); // 更新全局删除函数和 edges useEffect(() => { globalDeleteEdgeFn = handleDeleteEdge; globalEdges = edges; return () => { globalDeleteEdgeFn = null; globalEdges = []; }; }, [handleDeleteEdge, edges]); // 确保所有边都使用直线类型 useEffect(() => { setEdges((eds) => { const updatedEdges = eds.map(edge => { // 如果类型不是 straight,统一改为 straight if (edge.type !== 'straight') { return { ...edge, type: 'straight', markerStart: { type: 'arrowclosed', color: '#000000', }, }; } // 确保所有边都有箭头标记 if (!edge.markerStart) { return { ...edge, markerStart: { type: 'arrowclosed', color: '#000000', }, }; } return edge; }); // 检查是否有更新 const hasUpdate = updatedEdges.some((e, index) => e.type !== eds[index]?.type); if (hasUpdate) { return updatedEdges; } return eds; }); }, [setEdges]); // 删除节点 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); } setHasUnsavedChanges(true); 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); setHasUnsavedChanges(true); } }, [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); setHasUnsavedChanges(true); } }, [history, historyIndex, setNodes, setEdges]); // 网格步长,与画布 snapGrid 一致 const GRID_SIZE = 16; // 水平线磁吸阈值:放置时与已有节点 Y 相差在此范围内则对齐到该节点水平线 const HORIZONTAL_SNAP_THRESHOLD = 40; // 拖放处理:支持网格对齐 + 与前一个节点同一水平线磁吸 const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); const type = event.dataTransfer.getData('application/reactflow'); if (!type || !reactFlowInstance) return; let 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}`; setNodes((nds) => { // 网格对齐 position = { x: Math.round(position.x / GRID_SIZE) * GRID_SIZE, y: Math.round(position.y / GRID_SIZE) * GRID_SIZE, }; // 水平线磁吸:若有已有节点,且当前 Y 与最后一个节点 Y 接近,则对齐到同一水平线 if (nds.length > 0) { const lastNode = nds[nds.length - 1]; const refY = lastNode.position.y; if (Math.abs(position.y - refY) <= HORIZONTAL_SNAP_THRESHOLD) { position.y = refY; } } const nodeNumber = nds.length + 1; const newNode: Node = { id: nodeId, type, position: { ...position }, data: { label: config?.label || type, type, nodeId: String(nodeNumber).padStart(3, '0'), smsTemplateCode: 'false', messageTemplateCode: 'false', emailTemplateCode: 'false', appTemplateCode: 'false', }, }; 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; }); setHasUnsavedChanges(true); }, [reactFlowInstance, setNodes, history, historyIndex, edges] ); const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); // 拖拽节点时的水平线磁吸:与画布上其他节点 Y 接近时对齐到同一水平线;节点变更时标记为已修改 const onNodesChange = useCallback( (changes: NodeChange[]) => { const modified = changes.map((ch) => { if (ch.type === 'position' && ch.position != null && ch.dragging) { const otherNodes = nodes.filter((n) => n.id !== ch.id); let newY = ch.position!.y; for (const n of otherNodes) { if (Math.abs(ch.position!.y - n.position.y) <= HORIZONTAL_SNAP_THRESHOLD) { newY = n.position.y; break; } } return { ...ch, position: { ...ch.position!, y: newY } }; } return ch; }); if (!isLoadingFromServerOrRestoreRef.current) { setHasUnsavedChanges(true); } onNodesChangeBase(modified); }, [nodes, onNodesChangeBase] ); // 边变更时标记为已修改(仅用户操作会触发 onEdgesChange);加载/恢复阶段不标记 const onEdgesChangeWithDirty = useCallback( (changes: Parameters[0]) => { if (!isLoadingFromServerOrRestoreRef.current) { setHasUnsavedChanges(true); } onEdgesChange(changes); }, [onEdgesChange] ); // 更新节点配置 const updateNodeConfig = useCallback(() => { if (!selectedNode) return; setNodes((nds) => { const updatedNodes = nds.map((node) => { if (node.id === selectedNode.id) { const { isolationMethod, ...restNodeData } = node.data || {}; const updatedNode = { ...node, data: { ...restNodeData, label: nodeConfig.nodeName, type: nodeConfig.nodeIcon || node.data?.type, // 更新节点类型(图标) icon: nodeConfig.nodeIcon, // 保存图标名称 responsible: nodeConfig.responsible, remark: nodeConfig.remark, formId: nodeConfig.formId ? String(nodeConfig.formId) : '', isolationType: nodeConfig.isolationType, isolationPoints: nodeConfig.isolationPoints, isolationNode: nodeConfig.isolationNode, isolationNodeUuid: nodeConfig.isolationNodeUuid, lockPerson: nodeConfig.lockPerson, coLockPersons: nodeConfig.coLockPersons, notificationMethods: nodeConfig.notificationMethods, notificationPerson: nodeConfig.notificationPerson, notificationTime: nodeConfig.notificationTime, smsTemplateCode: nodeConfig.smsTemplateCode, messageTemplateCode: nodeConfig.messageTemplateCode, emailTemplateCode: nodeConfig.emailTemplateCode, appTemplateCode: nodeConfig.appTemplateCode, }, }; // 缓存当前节点配置 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; }); setHasUnsavedChanges(true); toast.success('节点配置已保存'); }, [selectedNode, nodeConfig, setNodes, setSelectedNode, history, historyIndex, edges]); // 保存流程(不跳转页面) const handleSaveWithoutNavigate = 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] || { parentUuid: [], childrenUuid: [] }; }); edges.forEach(e => { const sourceUuid = nodeIdMap.get(e.source) || e.source; const targetUuid = nodeIdMap.get(e.target) || e.target; if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { parentUuid: [], childrenUuid: [] }; if (!adjacency[targetUuid]) adjacency[targetUuid] = { parentUuid: [], childrenUuid: [] }; // 调换字段:原来childrenUuid的地方现在用parentUuid,原来parentUuid的地方现在用childrenUuid adjacency[sourceUuid].parentUuid.push(targetUuid); adjacency[targetUuid].childrenUuid.push(sourceUuid); }); const exportData = { nodeCount: nodes.length, edgeCount: edges.length, adjacency, nodes: nodes.map(n => { const cachedData = loadNodeCache(n.id); const mergedData = cachedData ? { ...n.data, ...cachedData } : n.data; // 将 responsible 转换为 workerUserId,并确保值是字符串 // 同时提取四个模板代码字段到顶层 const { responsible, smsTemplateCode, messageTemplateCode, emailTemplateCode, appTemplateCode, ...restData } = mergedData; const processedData = { ...restData, workerUserId: responsible !== undefined && responsible !== null && responsible !== '' ? String(responsible) : '', }; const nodeObj: any = { uuid: n.id, type: n.type, position: n.position, nodeName: processedData.label || '', nodeIcon: processedData.icon || processedData.type || n.type || '', smsTemplateCode: smsTemplateCode || 'false', messageTemplateCode: messageTemplateCode || 'false', emailTemplateCode: emailTemplateCode || 'false', appTemplateCode: appTemplateCode || 'false', data: processedData, }; 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 || ''; // 确保所有节点都包含模板代码字段,检查并修复缺失的字段 exportData.nodes.forEach((node: any) => { if (node.smsTemplateCode === undefined) node.smsTemplateCode = 'false'; if (node.messageTemplateCode === undefined) node.messageTemplateCode = 'false'; if (node.emailTemplateCode === undefined) node.emailTemplateCode = 'false'; if (node.appTemplateCode === undefined) node.appTemplateCode = 'false'; }); // 重新序列化 content,确保所有节点都包含模板代码字段 const finalContent = JSON.stringify(exportData, null, 2); // 调用更新接口,传递 id、content、name、description await workflowDesignApi.updateWorkflowDesign({ id: workflowId, content: finalContent, name: name, description: description, }); message.success('流程保存成功'); // 保存成功后清除缓存 clearWorkflowCache(workflowId); // 更新初始状态 initialNodesRef.current = JSON.parse(JSON.stringify(nodes)); initialEdgesRef.current = JSON.parse(JSON.stringify(edges)); setHasUnsavedChanges(false); } catch (error: any) { console.error('保存流程失败:', error); message.error(error?.message || '流程保存失败'); } }; // 保存流程 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] || { parentUuid: [], childrenUuid: [] }; }); edges.forEach(e => { const sourceUuid = nodeIdMap.get(e.source) || e.source; const targetUuid = nodeIdMap.get(e.target) || e.target; if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { parentUuid: [], childrenUuid: [] }; if (!adjacency[targetUuid]) adjacency[targetUuid] = { parentUuid: [], childrenUuid: [] }; // 调换字段:原来childrenUuid的地方现在用parentUuid,原来parentUuid的地方现在用childrenUuid adjacency[sourceUuid].parentUuid.push(targetUuid); adjacency[targetUuid].childrenUuid.push(sourceUuid); }); const exportData = { nodeCount: nodes.length, edgeCount: edges.length, adjacency, nodes: nodes.map(n => { const cachedData = loadNodeCache(n.id); const mergedData = cachedData ? { ...n.data, ...cachedData } : n.data; // 将 responsible 转换为 workerUserId,并确保值是字符串 // 同时提取四个模板代码字段到顶层 const { responsible, smsTemplateCode, messageTemplateCode, emailTemplateCode, appTemplateCode, ...restData } = mergedData; const processedData = { ...restData, workerUserId: responsible !== undefined && responsible !== null && responsible !== '' ? String(responsible) : '', }; const nodeObj: any = { uuid: n.id, type: n.type, position: n.position, nodeName: processedData.label || '', nodeIcon: processedData.icon || processedData.type || n.type || '', smsTemplateCode: smsTemplateCode || 'false', messageTemplateCode: messageTemplateCode || 'false', emailTemplateCode: emailTemplateCode || 'false', appTemplateCode: appTemplateCode || 'false', data: processedData, }; 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 || ''; // 确保所有节点都包含模板代码字段,检查并修复缺失的字段 exportData.nodes.forEach((node: any) => { if (node.smsTemplateCode === undefined) node.smsTemplateCode = 'false'; if (node.messageTemplateCode === undefined) node.messageTemplateCode = 'false'; if (node.emailTemplateCode === undefined) node.emailTemplateCode = 'false'; if (node.appTemplateCode === undefined) node.appTemplateCode = 'false'; }); // 重新序列化 content,确保所有节点都包含模板代码字段 const finalContent = JSON.stringify(exportData, null, 2); // 调用更新接口,传递 id、content、name、description await workflowDesignApi.updateWorkflowDesign({ id: workflowId, content: finalContent, name: name, description: description, }); // 保存成功后清除缓存 clearWorkflowCache(workflowId); // 更新初始状态 initialNodesRef.current = JSON.parse(JSON.stringify(nodes)); initialEdgesRef.current = JSON.parse(JSON.stringify(edges)); setHasUnsavedChanges(false); // 显示成功提示 message.success('流程保存成功'); // 等待提示显示后再跳转(给用户看到成功提示的时间) setTimeout(() => { // 保存成功后返回流程设计列表页面(与返回按钮逻辑相同) backToProcessTemplateMenu(); }, 500); } catch (error: any) { console.error('保存流程失败:', error); message.error(error?.message || '流程保存失败'); } }; // 返回(仅在有未保存修改时弹框:保存 或 放弃并返回) const handleBack = () => { if (hasUnsavedChanges) { Modal.confirm({ title: '未保存的更改', content: '检测到您有未保存的更改,请先保存后再返回,或放弃更改并返回。', okText: '保存', cancelText: '放弃并返回', maskClosable: false, closable: false, onOk: async () => { // 调用保存函数 try { // 构建导出数据(与handleSave中的逻辑相同) 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] || { parentUuid: [], childrenUuid: [] }; }); edges.forEach(e => { const sourceUuid = nodeIdMap.get(e.source) || e.source; const targetUuid = nodeIdMap.get(e.target) || e.target; if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { parentUuid: [], childrenUuid: [] }; if (!adjacency[targetUuid]) adjacency[targetUuid] = { parentUuid: [], childrenUuid: [] }; adjacency[sourceUuid].parentUuid.push(targetUuid); adjacency[targetUuid].childrenUuid.push(sourceUuid); }); const exportData = { nodeCount: nodes.length, edgeCount: edges.length, adjacency, nodes: nodes.map(n => { const cachedData = loadNodeCache(n.id); const mergedData = cachedData ? { ...n.data, ...cachedData } : n.data; // 将 responsible 转换为 workerUserId,并确保值是字符串 // 同时提取四个模板代码字段到顶层 const { responsible, smsTemplateCode, messageTemplateCode, emailTemplateCode, appTemplateCode, ...restData } = mergedData; const processedData = { ...restData, workerUserId: responsible !== undefined && responsible !== null && responsible !== '' ? String(responsible) : '', }; const nodeObj: any = { uuid: n.id, type: n.type, position: n.position, nodeName: processedData.label || '', nodeIcon: processedData.icon || processedData.type || n.type || '', smsTemplateCode: smsTemplateCode || 'false', messageTemplateCode: messageTemplateCode || 'false', emailTemplateCode: emailTemplateCode || 'false', appTemplateCode: appTemplateCode || 'false', data: processedData, }; 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, }; }), }; // 确保所有节点都包含模板代码字段,检查并修复缺失的字段 exportData.nodes.forEach((node: any) => { if (node.smsTemplateCode === undefined) node.smsTemplateCode = 'false'; if (node.messageTemplateCode === undefined) node.messageTemplateCode = 'false'; if (node.emailTemplateCode === undefined) node.emailTemplateCode = 'false'; if (node.appTemplateCode === undefined) node.appTemplateCode = 'false'; }); const content = JSON.stringify(exportData, null, 2); if (!workflowId) { message.warning('无法保存:缺少流程ID,请先创建流程'); return; } const name = workflowDetail?.name || ''; const description = workflowDetail?.description || ''; await workflowDesignApi.updateWorkflowDesign({ id: workflowId, content: content, name: name, description: description, }); clearWorkflowCache(workflowId); message.success('流程保存成功'); // 更新初始状态 initialNodesRef.current = JSON.parse(JSON.stringify(nodes)); initialEdgesRef.current = JSON.parse(JSON.stringify(edges)); setHasUnsavedChanges(false); // 保存成功后返回 setTimeout(() => { backToProcessTemplateMenu(); }, 500); } catch (error: any) { console.error('保存流程失败:', error); message.error(error?.message || '流程保存失败'); } }, onCancel: () => { // 放弃并返回:清除缓存后跳转到流程模板列表 clearWorkflowCache(workflowId); backToProcessTemplateMenu(); }, }); } else { // 没有未保存的更改,直接返回 backToProcessTemplateMenu(); } }; // 缩放控制 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] || { parentUuid: [], childrenUuid: [] }; }); edges.forEach(e => { const sourceUuid = nodeIdMap.get(e.source) || e.source; const targetUuid = nodeIdMap.get(e.target) || e.target; if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { parentUuid: [], childrenUuid: [] }; if (!adjacency[targetUuid]) adjacency[targetUuid] = { parentUuid: [], childrenUuid: [] }; // 调换字段:原来childrenUuid的地方现在用parentUuid,原来parentUuid的地方现在用childrenUuid adjacency[sourceUuid].parentUuid.push(targetUuid); adjacency[targetUuid].childrenUuid.push(sourceUuid); }); const exportData = { nodeCount: nodes.length, edgeCount: edges.length, adjacency, nodes: nodes.map(n => { // 从缓存中读取最新配置 const cachedData = loadNodeCache(n.id); // 合并缓存数据和节点数据,缓存数据优先级更高 const mergedData = cachedData ? { ...n.data, ...cachedData } : n.data; // 将 responsible 转换为 workerUserId,并确保值是字符串 // 同时提取四个模板代码字段到顶层 const { responsible, smsTemplateCode, messageTemplateCode, emailTemplateCode, appTemplateCode, ...restData } = mergedData; const processedData = { ...restData, workerUserId: responsible !== undefined && responsible !== null && responsible !== '' ? String(responsible) : '', }; // 导出时将id字段改为uuid,但值保持不变 // 将 nodeName 和 nodeIcon 从 data 中提取到顶层,方便后端识别,但保留 data 中的原始字段 const nodeObj: any = { uuid: n.id, // 字段名改为uuid,值保持不变 type: n.type, position: n.position, nodeName: processedData.label || '', // 从 data.label 提取到顶层作为 nodeName nodeIcon: processedData.icon || processedData.type || n.type || '', // 从 data.icon 或 data.type 提取到顶层作为 nodeIcon smsTemplateCode: smsTemplateCode || 'false', messageTemplateCode: messageTemplateCode || 'false', emailTemplateCode: emailTemplateCode || 'false', appTemplateCode: appTemplateCode || 'false', data: processedData, // 保留完整的 data,包括 label 和 icon,但 responsible 已转换为 workerUserId }; 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 || ''; // 从顶层读取四个模板代码字段(与 uuid 同级) const topLevelSmsTemplateCode = node.smsTemplateCode; const topLevelMessageTemplateCode = node.messageTemplateCode; const topLevelEmailTemplateCode = node.emailTemplateCode; const topLevelAppTemplateCode = node.appTemplateCode; // 兼容处理:优先使用 workerUserId,如果没有则使用 responsible(向后兼容),并确保值是字符串 const workerUserIdValue = nodeData.workerUserId !== undefined && nodeData.workerUserId !== null && nodeData.workerUserId !== '' ? String(nodeData.workerUserId) : (nodeData.responsible !== undefined && nodeData.responsible !== null && nodeData.responsible !== '' ? String(nodeData.responsible) : ''); // 确保所有字段都存在,使用默认值填充缺失的字段 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: workerUserIdValue, // 内部仍使用 responsible 字段名,但值来自 workerUserId 或 responsible workerUserId: workerUserIdValue, // 同时保存 workerUserId 字段,确保数据一致性 remark: nodeData.remark || '', formId: nodeData.formId || nodeData.submitForm ? String(nodeData.formId || nodeData.submitForm) : '', // 兼容旧的 submitForm 字段,确保是字符串类型 isolationType: nodeData.isolationType || '', isolationPoints: Array.isArray(nodeData.isolationPoints) ? nodeData.isolationPoints : [], isolationNode: Array.isArray(nodeData.isolationNode) ? nodeData.isolationNode : [], isolationNodeUuid: nodeData.isolationNodeUuid || '', 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 || '', smsTemplateCode: topLevelSmsTemplateCode !== undefined ? topLevelSmsTemplateCode : (nodeData.smsTemplateCode || 'false'), messageTemplateCode: topLevelMessageTemplateCode !== undefined ? topLevelMessageTemplateCode : (nodeData.messageTemplateCode || 'false'), emailTemplateCode: topLevelEmailTemplateCode !== undefined ? topLevelEmailTemplateCode : (nodeData.emailTemplateCode || 'false'), appTemplateCode: topLevelAppTemplateCode !== undefined ? topLevelAppTemplateCode : (nodeData.appTemplateCode || 'false'), ...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: '#000000' }, markerStart: { type: 'arrowclosed', color: '#000000', }, data: { fixedLength: edge.data?.fixedLength || 150, // 保留导入的 fixedLength 或设置为默认值 150 }, }; }); // 更新节点和边 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 || '', formId: source.formId || source.submitForm ? String(source.formId || source.submitForm) : '', // 兼容旧的 submitForm 字段,确保是字符串类型 isolationType: source.isolationType || '', isolationPoints: source.isolationPoints || [], isolationNode: Array.isArray(source.isolationNode) ? source.isolationNode : (source.isolationNode ? [source.isolationNode] : []), isolationNodeUuid: source.isolationNodeUuid || '', lockPerson: source.lockPerson || '', coLockPersons: source.coLockPersons || [], notificationMethods: source.notificationMethods || { sms: false, message: false, email: false, app: false, }, notificationPerson: source.notificationPerson || '', notificationTime: source.notificationTime || '', smsTemplateCode: source.smsTemplateCode || 'false', messageTemplateCode: source.messageTemplateCode || 'false', emailTemplateCode: source.emailTemplateCode || 'false', appTemplateCode: source.appTemplateCode || 'false', }); setSelectedNode(updatedNode); } else { // 如果选中的节点不在导入的数据中,清除选择 setSelectedNode(null); } } // 保存历史 const newHistory = [{ nodes: importedNodes, edges: importedEdges }]; setHistory(newHistory); setHistoryIndex(0); // 更新初始状态,用于检测未保存的更改 initialNodesRef.current = JSON.parse(JSON.stringify(importedNodes)); initialEdgesRef.current = JSON.parse(JSON.stringify(importedEdges)); setHasUnsavedChanges(false); 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 (
{/* 顶部工具栏 */}
流程设计器
{workflowDetail?.name && ( <> {workflowDetail.name}
)}
{/* 主内容区 */}
{/* 左侧节点面板 */}
{nodeConfigs.filter(config => config.type !== 'returnLock').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} connectionRadius={40} snapToGrid={true} snapGrid={[16, 16]} defaultEdgeOptions={{ style: { strokeWidth: 2, stroke: '#000000' }, markerStart: { type: 'arrowclosed', color: '#000000', }, // 不设置默认type,让onConnect中的逻辑决定 }} 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" />
{/* 右侧属性面板 */} {!rightPanelCollapsed && (
{selectedNode ? (
{/* 头部 */}

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

{/* 内容 */}
{ // 如果是创建作业节点,不允许切换到"提交表单"tab if (key === 'form' && selectedNode.data?.type === 'createJob') { setActiveTabKey('info'); } else { setActiveTabKey(key); } }} className="[&_.ant-tabs-tab]:!px-4 [&_.ant-tabs-tab]:!py-2.5 [&_.ant-tabs-tab-active]:!text-blue-600 [&_.ant-tabs-tab-active]:!font-semibold [&_.ant-tabs-ink-bar]:!bg-blue-600 [&_.ant-tabs-tab]:!text-gray-600 [&_.ant-tabs-tab]:!border-b-2 [&_.ant-tabs-tab]:!border-transparent [&_.ant-tabs-tab:hover]:!text-blue-500" items={[ { key: 'info', label: '节点信息', children: (
{/* 节点名称 */}
{/* 描述 - 创建作业、确认、审核、录入信息、隔离方案和解除隔离节点显示在节点名称顶部 */} {(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" />
{/* 底部描述已移至顶部,不再在此显示 */} {/* 显示图标 */}
{['开始', '确认', '审核', '录入', '能量隔离', '解除隔离', '结束'].map((category) => { const iconPaths = generateIconPaths(category); return (
{/* 分类标题带蓝色装饰条 */}
{category}
{/* 图标网格 */} {iconPaths.length > 0 ? (
{iconPaths.map((iconItem) => { const isSelected = nodeConfig.nodeIcon === iconItem.fileName; return (
{ // 保存文件名而不是路径 setNodeConfig({ ...nodeConfig, nodeIcon: iconItem.fileName }); setIconPickerOpen(false); // 立即更新节点显示 setNodes((nds) => { return nds.map((node) => { if (node.id === selectedNode.id) { return { ...node, data: { ...node.data, icon: iconItem.fileName, // 保存文件名 }, }; } return node; }); }); }} className={` rounded-lg border-2 cursor-pointer transition-all flex items-center justify-center overflow-hidden hover:border-blue-400 hover:bg-blue-50 ${isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 bg-white'} `} style={{ width: '25px', height: '25px' }} title={category} > {category} { // 如果图片加载失败,显示占位符 console.error('ProcessDesigner: 图标加载失败', { path: iconItem.path, id: iconItem.id, category }); const target = e.target as HTMLImageElement; target.style.display = 'none'; // 显示错误占位符 const parent = target.parentElement; if (parent && !parent.querySelector('.error-placeholder')) { const placeholder = document.createElement('div'); placeholder.className = 'error-placeholder text-xs text-gray-400'; placeholder.textContent = '?'; parent.appendChild(placeholder); } }} onLoad={() => { // console.log('ProcessDesigner: 图标加载成功', iconItem.path); }} />
); })}
) : (
该分类暂无图标(请检查控制台日志)
)}
); })}
} trigger="click" open={iconPickerOpen} onOpenChange={setIconPickerOpen} placement="right" >
{(() => { // 如果设置了自定义图标(图片文件名),显示图片 if (nodeConfig.nodeIcon && /^\d+\.png$/.test(nodeConfig.nodeIcon)) { const iconPath = getIconPathByFileName(nodeConfig.nodeIcon); if (iconPath) { return ( <>
节点图标 { // 如果图片加载失败,显示默认图标 console.error('图标加载失败:', nodeConfig.nodeIcon); }} />
选择图标 ); } } // 否则使用默认图标 let Icon = FileTextOutlined; let config = null; 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') && (
)} {/* 盲板和拆除:显示负责人 */} {/* 字典值:0=盲板,1=上锁挂牌,2=拆除 */} {(nodeConfig.isolationType === '0' || nodeConfig.isolationType === '2') && (

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

)} {/* 上锁挂牌:显示上锁人和共锁人 */} {nodeConfig.isolationType === '1' && ( <>
)} )}
), }, { key: 'notification', label: '通知消息', children: (
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, sms: e.target.checked, }, smsTemplateCode: e.target.checked ? 'true' : 'false', }) } > 短信
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, message: e.target.checked, }, messageTemplateCode: e.target.checked ? 'true' : 'false', }) } > 站内信
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, email: e.target.checked, }, emailTemplateCode: e.target.checked ? 'true' : 'false', }) } > 邮件
setNodeConfig({ ...nodeConfig, notificationMethods: { ...nodeConfig.notificationMethods, app: e.target.checked, }, appTemplateCode: e.target.checked ? 'true' : 'false', }) } > APP通知
{/*
*/} {/*
*/}
), }, ].filter(item => { // 如果是创建作业节点,过滤掉提交表单tab if (item.key === 'form' && selectedNode.data?.type === 'createJob') { return false; } return true; })} />
) : (
{/* 头部 */}
{/* 内容 */}

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

)}
)} {/* 收缩状态下的展开按钮 */} {rightPanelCollapsed && (
)}
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" />
{/* 表单预览Modal */} { setFormPreviewVisible(false); setFormPreviewData(null); setFormPreviewDetailData({ rule: [], option: {} }); }} footer={[ ]} width={800} style={{ top: 20 }} styles={{ body: { maxHeight: 'calc(90vh - 120px)', overflowY: 'auto', overflowX: 'hidden' } }} >
{(() => { const formConfig = formPreviewDetailData.option?.formConfig || defaultFormConfig; const layoutColumns = formConfig.layoutColumns || 1; // 渲染字段预览(支持嵌套结构) const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => { const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined); // 处理容器类型(card 和 grid) if (field.type === 'card') { const children = field.children || []; // 优先使用 label(字段名称),如果没有则使用 cardTitle,最后使用默认值 const cardTitle = field.label || field.cardTitle || '卡片容器'; return (
{children.map((child: any) => renderFieldPreview(child))}
); } if (field.type === 'grid') { const gridColumns = field.gridColumns || 2; const children = field.children || []; return (
{children.map((child: any) => { const childSpanStyle = gridColumns > 1 ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` } : undefined; return (
{renderFieldPreview(child)}
); })}
); } // 处理 alert 类型 if (field.type === 'alert') { return (
); } // 处理普通字段 switch (field.type) { case 'textarea': return (
); case 'number': return (
); case 'select': return (
); case 'date': return (
); case 'switch': return (
); case 'radio': return (
{(field.options || []).map((opt: any, idx: number) => ( {opt.label} ))}
); case 'checkbox': return (
{(field.options || []).map((opt: any, idx: number) => ( {opt.label} ))}
); default: return (
); } }; // 预览模式下,强制使用更小的 label 宽度,让输入区域更宽 const previewLabelSpan = formConfig.labelPosition !== 'top' ? (formConfig.labelWidth ? Math.min(Math.floor(formConfig.labelWidth / 8), 4) : 4) : undefined; const previewWrapperSpan = previewLabelSpan ? 24 - previewLabelSpan : undefined; return ( <>
1 ? { display: 'grid', gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`, gap: '16px' } : {}}> {(formPreviewDetailData.rule || []).map((field: any) => renderFieldPreview(field))}
); })()}
); }