| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840 |
- 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<string, React.ComponentType<any>> = {
- 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 (
- <div
- className={`relative px-4 py-4 rounded-lg shadow-sm border-2 w-[180px] h-auto min-h-[140px] bg-white ${
- selected
- ? 'border-blue-500 shadow-md ring-1 ring-blue-200'
- : 'border-gray-200 hover:border-gray-300'
- } transition-all`}
- >
- {/* 连接点 - 四个方向,每个方向都有唯一的 source 和 target Handle */}
- <Handle
- id="top-source"
- type="source"
- position={Position.Top}
- className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
- style={{ top: -6, left: '50%', transform: 'translateX(-50%)' }}
- isConnectable={true}
- />
- <Handle
- id="top-target"
- type="target"
- position={Position.Top}
- className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
- style={{ top: -6, left: '50%', transform: 'translateX(-50%)' }}
- isConnectable={true}
- />
- <Handle
- id="bottom-source"
- type="source"
- position={Position.Bottom}
- className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
- style={{ bottom: -6, left: '50%', transform: 'translateX(-50%)' }}
- isConnectable={true}
- />
- <Handle
- id="bottom-target"
- type="target"
- position={Position.Bottom}
- className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
- style={{ bottom: -6, left: '50%', transform: 'translateX(-50%)' }}
- isConnectable={true}
- />
- <Handle
- id="left-source"
- type="source"
- position={Position.Left}
- className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
- style={{ left: -6, top: '50%', transform: 'translateY(-50%)' }}
- isConnectable={true}
- />
- <Handle
- id="left-target"
- type="target"
- position={Position.Left}
- className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
- style={{ left: -6, top: '50%', transform: 'translateY(-50%)' }}
- isConnectable={true}
- />
- <Handle
- id="right-source"
- type="source"
- position={Position.Right}
- className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
- style={{ right: -6, top: '50%', transform: 'translateY(-50%)' }}
- isConnectable={true}
- />
- <Handle
- id="right-target"
- type="target"
- position={Position.Right}
- className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
- style={{ right: -6, top: '50%', transform: 'translateY(-50%)' }}
- isConnectable={true}
- />
-
- {/* 垂直布局:顶部图标、中间名称、底部ID */}
- <div className="flex flex-col items-center justify-between gap-3 h-full">
- {/* 顶部:图标 */}
- <div className={`${config?.bgColor || 'bg-gray-50'} ${config?.borderColor || 'border-gray-100'} border p-3 rounded-xl shadow-sm flex items-center justify-center flex-shrink-0`} style={{ borderRadius: '12px' }}>
- <Icon
- className={`${config?.iconColor || 'text-gray-600'} text-2xl`}
- style={{ color: config?.iconColorCustom || undefined }}
- />
- </div>
- {/* 中间:节点名称 */}
- <div className="font-semibold text-sm text-gray-900 leading-tight text-center break-words w-full flex-1 flex items-center justify-center px-1">
- {data.label || config?.label}
- </div>
- {/* 底部:ID */}
- <div className="text-xs text-gray-500 text-center flex-shrink-0">
- ID: {nodeId}
- </div>
- </div>
- </div>
- );
- }
- // 各个节点类型
- function CreateJobNode(props: any) {
- return <CustomNode {...props} />;
- }
- function ConfirmNode(props: any) {
- return <CustomNode {...props} />;
- }
- function ReviewNode(props: any) {
- return <CustomNode {...props} />;
- }
- function InputInfoNode(props: any) {
- return <CustomNode {...props} />;
- }
- function IsolationNode(props: any) {
- return <CustomNode {...props} />;
- }
- function ReleaseIsolationNode(props: any) {
- return <CustomNode {...props} />;
- }
- function ReturnLockNode(props: any) {
- return <CustomNode {...props} />;
- }
- function CompleteNode(props: any) {
- return <CustomNode {...props} />;
- }
- // 自定义节点类型映射
- 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 (
- <>
- <BaseEdge
- id={id}
- path={edgePath}
- markerEnd={markerEnd}
- style={{
- ...style,
- strokeWidth: selected ? 3 : 2,
- stroke: selected ? '#3b82f6' : '#94a3b8',
- }}
- />
- {selected && (
- <EdgeLabelRenderer>
- <div
- style={{
- position: 'absolute',
- left: labelX,
- top: labelY,
- transform: 'translate(-50%, -50%)',
- pointerEvents: 'all',
- zIndex: 1000,
- }}
- className="nodrag nopan"
- >
- <button
- onClick={(e) => {
- e.stopPropagation();
- e.preventDefault();
- console.log('删除按钮被点击,连线ID:', id);
- if (globalDeleteEdgeFn) {
- globalDeleteEdgeFn(id);
- }
- }}
- onMouseDown={(e) => {
- e.stopPropagation();
- }}
- className="bg-red-500 hover:bg-red-600 rounded-full p-1.5 shadow-lg transition-colors flex items-center justify-center w-8 h-8"
- title="删除连线"
- type="button"
- style={{ cursor: 'pointer' }}
- >
- <DeleteOutlined className="text-sm text-white" style={{ color: 'white' }} />
- </button>
- </div>
- </EdgeLabelRenderer>
- )}
- </>
- );
- }
- // 自定义边类型映射 - 定义在组件外部
- 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<Node | null>(null);
- const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
- const [activeTabKey, setActiveTabKey] = useState<string>('info');
- const reactFlowWrapper = useRef<HTMLDivElement>(null);
- const [reactFlowInstance, setReactFlowInstance] = useState<any>(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<WorkflowDesignVO | null>(null);
- const [loadingDetail, setLoadingDetail] = useState(false);
-
- // 角色用户列表
- const [drawerUsers, setDrawerUsers] = useState<UserVO[]>([]); // 负责人(jtdrawer)
- const [lockerUsers, setLockerUsers] = useState<UserVO[]>([]); // 上锁人(jtlocker)
- const [colockerUsers, setColockerUsers] = useState<UserVO[]>([]); // 共锁人(jtcolocker)
- // 隔离点列表
- const [isolationPoints, setIsolationPoints] = useState<SegregationPointVO[]>([]);
- // 表单列表
- const [formList, setFormList] = useState<FormVO[]>([]);
- 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<string, string>();
- 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<string, { incoming: string[]; outgoing: string[] }> = {};
- const nodeIdMap = new Map<string, string>();
- 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<string, string>();
- nodes.forEach(n => {
- nodeIdMap.set(n.id, n.id); // 值保持不变,只是字段名改为uuid
- });
- // adjacency使用uuid作为key
- const adjacency: Record<string, { incoming: string[]; outgoing: string[] }> = {};
- 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<string, string>();
- 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 (
- <div className="h-screen w-screen flex flex-col bg-gray-50">
- {/* 顶部工具栏 */}
- <div className="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-4 shadow-sm z-10">
- <div className="flex items-center gap-2">
- <span className="text-lg font-bold text-blue-600 mr-2">流程设计器</span>
- <div className="h-5 w-px bg-gray-300 mx-1" />
- <Button
- type="primary"
- className="flex items-center gap-1.5"
- onClick={handleSave}
- >
- <SaveOutlined />
- <span>保存</span>
- </Button>
- <div className="h-5 w-px bg-gray-300 mx-1" />
- <Button
- size="small"
- className="flex items-center gap-1.5"
- onClick={handleBack}
- >
- <ArrowLeftOutlined />
- <span>返回</span>
- </Button>
- <div className="h-5 w-px bg-gray-300 mx-1" />
- <Button
- size="small"
- className="flex items-center gap-1.5"
- onClick={handleUndo}
- disabled={historyIndex <= 0}
- >
- <UndoOutlined />
- <span>撤销</span>
- </Button>
- <Button
- size="small"
- className="flex items-center gap-1.5"
- onClick={handleRedo}
- disabled={historyIndex >= history.length - 1}
- >
- <RedoOutlined />
- <span>重做</span>
- </Button>
- <Button
- size="small"
- className="flex items-center gap-1.5"
- onClick={handleExportJson}
- >
- <DownloadOutlined />
- <span>导出JSON</span>
- </Button>
- <Button
- size="small"
- className="flex items-center gap-1.5"
- onClick={() => setImportVisible(true)}
- >
- <UploadOutlined />
- <span>导入JSON</span>
- </Button>
- <div className="h-5 w-px bg-gray-300 mx-1" />
- <Button
- size="small"
- icon={<ZoomOutOutlined />}
- onClick={handleZoomOut}
- />
- <span className="text-sm text-gray-600 px-2 min-w-[50px] text-center">
- {Math.round(zoom * 100)}%
- </span>
- <Button
- size="small"
- icon={<ZoomInOutlined />}
- onClick={handleZoomIn}
- />
- </div>
- </div>
- {/* 主内容区 */}
- <div className="flex-1 flex overflow-hidden">
- {/* 左侧节点面板 */}
- <div className="w-56 bg-gray-50 border-r border-gray-200 overflow-y-auto">
- <div className="p-2 space-y-3">
- {nodeConfigs.map((config) => {
- const Icon = config.icon;
- return (
- <div
- key={config.type}
- draggable
- onDragStart={(e) => 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
- }}
- >
- <Icon
- className={`${config.iconColor} text-2xl`}
- style={{ color: config.iconColorCustom || undefined }}
- />
- <span className="text-xs font-medium text-center leading-tight" style={{ color: '#6b7280' }}>
- {config.label}
- </span>
- </div>
- );
- })}
- </div>
- </div>
- {/* 中间画布 */}
- <div className="flex-1 relative bg-white" ref={reactFlowWrapper}>
- <ReactFlow
- nodes={nodes}
- edges={edges}
- onNodesChange={onNodesChange}
- onEdgesChange={onEdgesChange}
- onConnect={onConnect}
- onEdgeUpdate={onEdgeUpdate}
- isValidConnection={isValidConnection}
- onNodeClick={onNodeClick}
- onNodeContextMenu={onNodeContextMenu}
- onEdgeClick={onEdgeClick}
- onPaneClick={onPaneClick}
- onDrop={onDrop}
- onDragOver={onDragOver}
- deleteKeyCode={['Delete', 'Backspace']}
- nodeTypes={nodeTypes as NodeTypes}
- edgeTypes={edgeTypes}
- fitView
- onInit={setReactFlowInstance}
- onMove={(_, viewport) => setZoom(viewport.zoom)}
- connectionMode={ConnectionMode.Loose}
- defaultEdgeOptions={{
- style: { strokeWidth: 2, stroke: '#94a3b8' },
- type: 'straight',
- }}
- edgesUpdatable={true}
- edgesFocusable={true}
- edgeUpdaterRadius={10}
- >
- <Controls className="!bg-white !border !border-gray-200 !rounded-lg !shadow-md" />
- <Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#e5e7eb" />
- <MiniMap
- nodeColor={(node) => {
- 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"
- />
- </ReactFlow>
- </div>
- {/* 右侧属性面板 */}
- <div className="w-80 bg-gray-50 border-l border-gray-200 overflow-y-auto">
- {selectedNode ? (
- <div className="h-full flex flex-col">
- {/* 头部 */}
- <div className="p-4 bg-white border-b border-gray-200 flex items-center justify-between sticky top-0 z-10 shadow-sm">
- <h3 className="text-base font-semibold text-gray-900">
- {nodeConfig.nodeName || selectedNode.data?.label || '节点'} 设置
- </h3>
- <div className="flex items-center gap-2">
- <Button
- type="text"
- danger
- size="small"
- onClick={() => handleDeleteNode()}
- className="text-red-500 hover:text-red-600"
- icon={<DeleteOutlined />}
- />
- <button
- onClick={() => setSelectedNode(null)}
- className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
- >
- <CloseOutlined className="text-gray-500" />
- </button>
- </div>
- </div>
- {/* 内容 */}
- <div className="flex-1 p-4 overflow-y-auto">
- <Tabs
- activeKey={activeTabKey}
- onChange={setActiveTabKey}
- 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: (
- <div className="space-y-5">
- {/* 节点名称 */}
- <div>
- {/* 描述 - 创建作业、确认、审核、录入信息、隔离方案和解除隔离节点显示在节点名称顶部 */}
- {(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') && (
- <div className="text-xs text-gray-500 leading-relaxed mb-2">
- {getNodeDescription(selectedNode.data?.type || '')}
- </div>
- )}
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 节点名称 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
- </label>
- {selectedNode.data?.type !== 'createJob' && selectedNode.data?.type !== 'confirm' && (
- <p className="text-xs text-gray-500 mb-2.5 leading-relaxed">
- (默认名称: {nodeConfigs.find(c => c.type === selectedNode.data?.type)?.label || '节点名称'},可根据需求调整)
- </p>
- )}
- {selectedNode.data?.type === 'createJob' && (
- <p className="text-xs text-gray-500 mb-2.5 leading-relaxed">
- (默认名称: {nodeConfigs.find(c => c.type === selectedNode.data?.type)?.label || '节点名称'},可根据需求调整)
- </p>
- )}
- <Input
- value={nodeConfig.nodeName || ''}
- onChange={(e) =>
- setNodeConfig({ ...nodeConfig, nodeName: e.target.value })
- }
- placeholder="请输入节点名称"
- className="rounded-lg border-gray-200 h-10"
- />
- </div>
- {/* 底部描述已移至顶部,不再在此显示 */}
- {/* 显示图标 */}
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 显示图标
- </label>
- <Popover
- content={
- <div style={{ width: '300px' }}>
- <div className="grid grid-cols-5 gap-3 p-3" style={{ gridTemplateColumns: 'repeat(5, 1fr)' }}>
- {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 (
- <div
- key={iconItem.name}
- onClick={() => {
- 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' }}
- >
- <IconComponent className="text-xl text-gray-700" />
- </div>
- );
- })}
- </div>
- </div>
- }
- title="选择图标"
- trigger="click"
- open={iconPickerOpen}
- onOpenChange={setIconPickerOpen}
- placement="right"
- >
- <div className="border border-gray-200 rounded-lg p-3 bg-white flex items-center gap-3 cursor-pointer hover:border-blue-400 transition-colors shadow-sm">
- {(() => {
- // 如果设置了自定义图标,使用自定义图标
- 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 (
- <>
- <div className={`${config?.bgColor || 'bg-gray-50'} ${config?.borderColor || 'border-gray-100'} border p-3 rounded-xl shadow-sm flex items-center justify-center`} style={{ borderRadius: '12px' }}>
- <Icon
- className={`${config?.iconColor || 'text-gray-600'} text-lg`}
- style={{ color: config?.iconColorCustom || undefined }}
- />
- </div>
- <span className="text-sm text-gray-600">选择图标</span>
- </>
- );
- })()}
- </div>
- </Popover>
- </div>
- {/* 负责人 - 确认节点显示在图标下方 */}
- {selectedNode.data?.type === 'confirm' && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 负责人
- </label>
- <Select
- value={nodeConfig.responsible || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, responsible: value || '' })
- }
- placeholder="请选择负责人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- {drawerUsers.map(user => (
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
- ))}
- </Select>
- <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">
- 对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择。
- </p>
- </div>
- )}
- {/* 负责人 - 审核节点显示在图标下方 */}
- {selectedNode.data?.type === 'review' && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 负责人
- </label>
- <Select
- value={nodeConfig.responsible || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, responsible: value || '' })
- }
- placeholder="请选择负责人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- {drawerUsers.map(user => (
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
- ))}
- </Select>
- <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">
- 对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择。
- </p>
- </div>
- )}
- {/* 负责人 - 创建作业、隔离方案和解除隔离节点不显示 */}
- {selectedNode.data?.type !== 'createJob' && selectedNode.data?.type !== 'confirm' && selectedNode.data?.type !== 'review' && selectedNode.data?.type !== 'isolation' && selectedNode.data?.type !== 'releaseIsolation' && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 负责人
- </label>
- <Select
- value={nodeConfig.responsible || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, responsible: value || '' })
- }
- placeholder="请选择负责人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- {drawerUsers.map(user => (
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
- ))}
- </Select>
- <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">
- 对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择
- </p>
- </div>
- )}
- {/* 备注 - 创建作业、确认、审核、录入信息、隔离方案、解除隔离、还锁和完成节点不显示 */}
- {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' && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 备注
- </label>
- <Input.TextArea
- value={nodeConfig.remark || ''}
- onChange={(e) =>
- setNodeConfig({ ...nodeConfig, remark: e.target.value })
- }
- placeholder="请输入备注"
- rows={3}
- className="rounded-lg border-gray-200"
- />
- </div>
- )}
- <div className="flex gap-2">
- <Button
- danger
- onClick={() => handleDeleteNode()}
- className="flex-1 rounded-lg"
- size="large"
- >
- 删除节点
- </Button>
- </div>
- </div>
- ),
- },
- {
- key: 'form',
- label: '提交表单',
- children: (
- <div className="space-y-5">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 提交表单 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
- </label>
- <Select
- value={nodeConfig.submitForm || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, submitForm: value || '' })
- }
- placeholder="请选择提交表单"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- {formList.map(form => (
- <Select.Option key={form.id} value={form.id}>{form.name}</Select.Option>
- ))}
- </Select>
- </div>
- {/* 隔离/方案 和 解除隔离 节点特有的字段 */}
- {(selectedNode.data?.type === 'isolation' || selectedNode.data?.type === 'releaseIsolation') && (
- <>
- {/* 解除隔离节点:选择隔离节点 */}
- {selectedNode.data?.type === 'releaseIsolation' && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 选择隔离节点
- </label>
- <Select
- value={nodeConfig.selectedIsolationNodeId || undefined}
- onChange={(value) => {
- setNodeConfig((prev) => {
- const target = nodes.find(n => n.id === value && n.data?.type === 'isolation');
- if (target) {
- // 优先从缓存中读取,缓存中没有则从 node.data 读取
- const cache = loadNodeCache(target.id);
- const source = cache || target.data || {};
- return {
- ...prev,
- selectedIsolationNodeId: value || '',
- isolationMethod: source.isolationMethod || '',
- isolationPoints: source.isolationPoints || [],
- isolationNode: Array.isArray(source.isolationNode)
- ? source.isolationNode
- : (source.isolationNode ? [source.isolationNode] : []),
- responsible: source.responsible || '',
- lockPerson: source.lockPerson || '',
- coLockPersons: source.coLockPersons || [],
- };
- }
- return { ...prev, selectedIsolationNodeId: value || '' };
- });
- }}
- placeholder="请选择隔离节点"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- {nodes
- .filter(n => n.data?.type === 'isolation')
- .map(node => (
- <Select.Option key={node.id} value={node.id}>
- {node.data?.label || nodeConfigs.find(c => c.type === 'isolation')?.label || '隔离/方案'}
- </Select.Option>
- ))}
- </Select>
- </div>
- )}
- {/* 隔离方式 - 隔离方案节点可编辑,解除隔离节点只读(根据选择的隔离节点自动填充) */}
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 隔离方式
- </label>
- <Select
- value={nodeConfig.isolationMethod || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, isolationMethod: value || '' })
- }
- placeholder="请选择隔离方式"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={selectedNode.data?.type === 'releaseIsolation'}
- >
- <Select.Option value="blindPlate">盲板</Select.Option>
- <Select.Option value="lockTag">上锁挂牌</Select.Option>
- <Select.Option value="remove">拆除</Select.Option>
- </Select>
- </div>
- {/* 隔离点选择(可多选)- 隔离方案节点显示,解除隔离只读展示 */}
- {(selectedNode.data?.type === 'isolation' || selectedNode.data?.type === 'releaseIsolation') && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 隔离点选择(可多选)
- </label>
- <Select
- mode="multiple"
- value={nodeConfig.isolationPoints}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, isolationPoints: value })
- }
- placeholder="请选择隔离点"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!min-h-10"
- allowClear
- disabled={selectedNode.data?.type === 'releaseIsolation'}
- >
- {isolationPoints.map((point: any, index) => (
- <Select.Option key={point.pointId || point.id || `point-${index}`} value={point.pointId || point.id}>{point.pointName}</Select.Option>
- ))}
- </Select>
- </div>
- )}
- {/* 盲板和拆除:显示负责人 */}
- {(nodeConfig.isolationMethod === 'blindPlate' || nodeConfig.isolationMethod === 'remove') && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 负责人
- </label>
- <Select
- value={nodeConfig.responsible || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, responsible: value || '' })
- }
- placeholder="请选择负责人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={selectedNode.data?.type === 'releaseIsolation'}
- >
- {drawerUsers.map(user => (
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
- ))}
- </Select>
- <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">
- 对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择。
- </p>
- </div>
- )}
- {/* 上锁挂牌:显示上锁人和共锁人 */}
- {nodeConfig.isolationMethod === 'lockTag' && (
- <>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 上锁人
- </label>
- <Select
- value={nodeConfig.lockPerson || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, lockPerson: value || '' })
- }
- placeholder="请选择上锁人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={selectedNode.data?.type === 'releaseIsolation'}
- >
- {lockerUsers.map(user => (
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
- ))}
- </Select>
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 共锁人(可多选)
- </label>
- <Select
- mode="multiple"
- value={nodeConfig.coLockPersons}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, coLockPersons: value })
- }
- placeholder="请选择共锁人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!min-h-10"
- allowClear
- disabled={selectedNode.data?.type === 'releaseIsolation'}
- >
- {colockerUsers.map(user => (
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
- ))}
- </Select>
- </div>
- </>
- )}
- </>
- )}
- <div className="flex gap-2">
- <Button
- danger
- onClick={() => handleDeleteNode()}
- className="flex-1 rounded-lg"
- size="large"
- >
- 删除节点
- </Button>
- </div>
- </div>
- ),
- },
- {
- key: 'notification',
- label: '通知消息',
- children: (
- <div className="space-y-5">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-3">
- 通知方式
- </label>
- <div className="bg-gray-50 p-3 rounded-lg">
- <div className="mb-3">
- <Checkbox
- checked={nodeConfig.notificationMethods.sms}
- onChange={(e) =>
- setNodeConfig({
- ...nodeConfig,
- notificationMethods: {
- ...nodeConfig.notificationMethods,
- sms: e.target.checked,
- },
- })
- }
- >
- 短信
- </Checkbox>
- </div>
- <div className="mb-3">
- <Checkbox
- checked={nodeConfig.notificationMethods.message}
- onChange={(e) =>
- setNodeConfig({
- ...nodeConfig,
- notificationMethods: {
- ...nodeConfig.notificationMethods,
- message: e.target.checked,
- },
- })
- }
- >
- 站内信
- </Checkbox>
- </div>
- <div className="mb-3">
- <Checkbox
- checked={nodeConfig.notificationMethods.email}
- onChange={(e) =>
- setNodeConfig({
- ...nodeConfig,
- notificationMethods: {
- ...nodeConfig.notificationMethods,
- email: e.target.checked,
- },
- })
- }
- >
- 邮件
- </Checkbox>
- </div>
- <div>
- <Checkbox
- checked={nodeConfig.notificationMethods.app}
- onChange={(e) =>
- setNodeConfig({
- ...nodeConfig,
- notificationMethods: {
- ...nodeConfig.notificationMethods,
- app: e.target.checked,
- },
- })
- }
- >
- APP通知
- </Checkbox>
- </div>
- </div>
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 通知人
- </label>
- <Select
- value={nodeConfig.notificationPerson || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, notificationPerson: value || '' })
- }
- placeholder="请选择通知人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- <Select.Option value="taskResponsible">任务负责人</Select.Option>
- <Select.Option value="taskParticipant">任务参与人</Select.Option>
- <Select.Option value="responsibleAndParticipant">负责人和参与人</Select.Option>
- <Select.Option value="specifiedPerson">指定人</Select.Option>
- </Select>
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 通知时间
- </label>
- <Select
- value={nodeConfig.notificationTime || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, notificationTime: value || '' })
- }
- placeholder="请选择通知时间"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- <Select.Option value="before">执行前(上一个节点结束后)</Select.Option>
- <Select.Option value="after">执行后(该节点结束后)</Select.Option>
- <Select.Option value="time">选择时间</Select.Option>
- <Select.Option value="30min">任务开始前30分钟</Select.Option>
- <Select.Option value="1h">任务开始前1小时</Select.Option>
- <Select.Option value="2h">任务开始前2小时</Select.Option>
- <Select.Option value="4h">任务开始前4小时</Select.Option>
- <Select.Option value="5h">任务开始前5小时</Select.Option>
- <Select.Option value="8h">任务开始前8小时</Select.Option>
- <Select.Option value="12h">任务开始前12小时</Select.Option>
- <Select.Option value="24h">任务开始前24小时</Select.Option>
- <Select.Option value="48h">任务开始前48小时</Select.Option>
- </Select>
- </div>
- <div className="flex gap-2">
- <Button
- danger
- onClick={() => handleDeleteNode()}
- className="flex-1 rounded-lg"
- size="large"
- >
- 删除节点
- </Button>
- </div>
- </div>
- ),
- },
- // 通知消息tab - 已隐藏但保留代码
- {
- key: 'notification',
- label: '通知消息',
- children: (
- <div className="space-y-5">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-3">
- 通知方式
- </label>
- <div className="bg-gray-50 p-3 rounded-lg">
- <div className="mb-3">
- <Checkbox
- checked={nodeConfig.notificationMethods.sms}
- onChange={(e) =>
- setNodeConfig({
- ...nodeConfig,
- notificationMethods: {
- ...nodeConfig.notificationMethods,
- sms: e.target.checked,
- },
- })
- }
- >
- 短信
- </Checkbox>
- </div>
- <div className="mb-3">
- <Checkbox
- checked={nodeConfig.notificationMethods.message}
- onChange={(e) =>
- setNodeConfig({
- ...nodeConfig,
- notificationMethods: {
- ...nodeConfig.notificationMethods,
- message: e.target.checked,
- },
- })
- }
- >
- 站内信
- </Checkbox>
- </div>
- <div className="mb-3">
- <Checkbox
- checked={nodeConfig.notificationMethods.email}
- onChange={(e) =>
- setNodeConfig({
- ...nodeConfig,
- notificationMethods: {
- ...nodeConfig.notificationMethods,
- email: e.target.checked,
- },
- })
- }
- >
- 邮件
- </Checkbox>
- </div>
- <div>
- <Checkbox
- checked={nodeConfig.notificationMethods.app}
- onChange={(e) =>
- setNodeConfig({
- ...nodeConfig,
- notificationMethods: {
- ...nodeConfig.notificationMethods,
- app: e.target.checked,
- },
- })
- }
- >
- APP通知
- </Checkbox>
- </div>
- </div>
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 通知人
- </label>
- <Select
- value={nodeConfig.notificationPerson || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, notificationPerson: value || '' })
- }
- placeholder="请选择通知人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- <Select.Option value="taskResponsible">任务负责人</Select.Option>
- <Select.Option value="taskParticipant">任务参与人</Select.Option>
- <Select.Option value="responsibleAndParticipant">负责人和参与人</Select.Option>
- <Select.Option value="specifiedPerson">指定人</Select.Option>
- </Select>
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 通知时间
- </label>
- <Select
- value={nodeConfig.notificationTime || undefined}
- onChange={(value) =>
- setNodeConfig({ ...nodeConfig, notificationTime: value || '' })
- }
- placeholder="请选择通知时间"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- <Select.Option value="before">执行前(上一个节点结束后)</Select.Option>
- <Select.Option value="after">执行后(该节点结束后)</Select.Option>
- <Select.Option value="time">选择时间</Select.Option>
- <Select.Option value="30min">任务开始前30分钟</Select.Option>
- <Select.Option value="1h">任务开始前1小时</Select.Option>
- <Select.Option value="2h">任务开始前2小时</Select.Option>
- <Select.Option value="4h">任务开始前4小时</Select.Option>
- <Select.Option value="5h">任务开始前5小时</Select.Option>
- <Select.Option value="8h">任务开始前8小时</Select.Option>
- <Select.Option value="12h">任务开始前12小时</Select.Option>
- <Select.Option value="24h">任务开始前24小时</Select.Option>
- <Select.Option value="48h">任务开始前48小时</Select.Option>
- </Select>
- </div>
- <div className="flex gap-2">
- <Button
- danger
- onClick={() => handleDeleteNode()}
- className="flex-1 rounded-lg"
- size="large"
- >
- 删除节点
- </Button>
- </div>
- </div>
- ),
- },
- ].filter(item => item.key !== 'notification')}
- />
- </div>
- </div>
- ) : (
- <div className="p-4 text-center text-gray-500 mt-20">
- <p className="text-sm">请点击画布中的节点查看和编辑属性</p>
- </div>
- )}
- </div>
- </div>
- <Modal
- open={exportVisible}
- title="流程 JSON 导出"
- onCancel={() => setExportVisible(false)}
- onOk={() => setExportVisible(false)}
- width={800}
- okText="关闭"
- cancelText="取消"
- style={{ top: 20 }}
- styles={{ body: { maxHeight: 'calc(90vh - 120px)', overflowY: 'auto', overflowX: 'hidden', padding: 0 } }}
- >
- <div className="relative">
- {/* 固定在顶部的复制按钮 */}
- <div className="sticky top-0 bg-white z-10 px-6 py-3 flex justify-end">
- <Button
- type="default"
- icon={<CopyOutlined />}
- onClick={handleCopyJson}
- >
- 复制JSON
- </Button>
- </div>
- {/* JSON内容区域 */}
- <div className="p-6">
- <div className="bg-gray-50 border border-gray-200 rounded-lg p-3 overflow-x-hidden">
- <pre className="text-xs text-gray-800 whitespace-pre-wrap break-words font-mono overflow-x-hidden">
- {exportContent}
- </pre>
- </div>
- </div>
- </div>
- </Modal>
- <Modal
- open={importVisible}
- title="导入流程 JSON"
- onCancel={() => {
- setImportVisible(false);
- setImportJson('');
- }}
- onOk={handleImportJson}
- width={800}
- okText="导入"
- cancelText="取消"
- >
- <div className="space-y-3">
- <p className="text-sm text-gray-600">
- 请粘贴流程 JSON 数据,导入后将替换当前画布内容
- </p>
- <Input.TextArea
- value={importJson}
- onChange={(e) => setImportJson(e.target.value)}
- placeholder="请粘贴 JSON 数据..."
- rows={15}
- className="font-mono text-xs"
- />
- </div>
- </Modal>
- </div>
- );
- }
|