| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534 |
- import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
- import { useNavigate } from 'react-router-dom';
- import { useTranslation } from 'react-i18next';
- import { Plus, Search, Edit2, Trash2, MoreVertical, FileText, Eye, Play, CheckCircle, RefreshCw, Workflow, Hand } from 'lucide-react';
- import { Button, Input, Space, Select, Table as AntdTable, message, Modal, Form, Row, Col, Tabs, Radio, DatePicker, Checkbox, Tooltip, Switch as AntdSwitch } from 'antd';
- import { Button as UIButton } from './ui/button';
- import type { ColumnsType } from 'antd/es/table';
- import { workflowDesignApi, WorkflowDesignVO } from '../api/WorkflowDesign';
- import { workJobApi, WorkJobVO, WorkflowWorkNodeDO, UpdateWorkflowWorkNodeParam, WorkflowWorkNodeUserDO, UpdateStartWorkParam } from '../api/WorkJob';
- import { dictDataApi, DictDataVO } from '../api/DictData';
- import ReactFlow, {
- Node,
- Edge,
- useNodesState,
- useEdgesState,
- Controls,
- Background,
- NodeTypes,
- BackgroundVariant,
- Handle,
- Position,
- ConnectionMode,
- BaseEdge,
- getStraightPath,
- EdgeTypes,
- } from 'reactflow';
- import 'reactflow/dist/style.css';
- import {
- ToolOutlined,
- CheckCircleOutlined,
- FileTextOutlined,
- EditOutlined,
- SafetyOutlined,
- UnlockOutlined,
- LockOutlined,
- CheckSquareOutlined,
- CloseOutlined,
- DeleteOutlined,
- ExclamationCircleOutlined,
- ClockCircleOutlined,
- FireOutlined,
- WarningOutlined,
- } from '@ant-design/icons';
- import { userApi, UserVO } from '../api/user';
- import { segregationPointApi, SegregationPointVO } from '../api/spm';
- import { getFormPage, getForm, FormVO } from '../api/bpm/form';
- import urgecy1Icon from '../assets/urgecy1.png';
- import urgecy2Icon from '../assets/urgecy2.png';
- import urgecy3Icon from '../assets/urgecy3.png';
- interface TableRow {
- id: number;
- [key: string]: any;
- }
- interface IsolationWorkProps {
- subMenu: string;
- }
- // 节点配置(从ProcessDesigner复制)
- 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',
- },
- ];
- // 使用 import.meta.glob 动态导入所有图标(用于节点显示)
- const iconModulesForNode = import.meta.glob('../assets/节点图标/**/*.png', { eager: true, as: 'url' });
- // 根据文件名获取图标路径(用于节点显示)
- 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(iconModulesForNode);
- const matchingKey = allKeys.find(k => {
- const normalizedKey = k.replace(/\\/g, '/').toLowerCase();
- return normalizedKey.includes(category.toLowerCase()) && normalizedKey.endsWith(actualFileName.toLowerCase());
- });
-
- if (matchingKey) {
- return iconModulesForNode[matchingKey] as string;
- }
-
- return null;
- };
- // 自定义节点组件
- function CustomNode({ data, selected, id, nodeSavedStatusCache }: any) {
- const config = nodeConfigs.find(c => c.type === data.type);
- const Icon = config?.icon || FileTextOutlined;
- const nodeId = data.nodeId || (id ? String(parseInt(id.split('-').pop() || '0') % 1000).padStart(3, '0') : '001');
- // 优先使用缓存中的保存状态,如果没有则使用 data.completed
- const isSaved = nodeSavedStatusCache ? (nodeSavedStatusCache.get(id) || false) : (data.completed || false);
- const isCompleted = data.completed || false;
-
- // 检查是否是图片文件名(如 "1000.png")
- let iconImagePath: string | null = null;
- // 支持多种格式:纯文件名(如 "1000.png")或完整路径
- if (data.icon) {
- // 如果是纯文件名格式
- if (/^\d+\.png$/.test(data.icon)) {
- iconImagePath = getIconPathByFileName(data.icon);
- }
- // 如果已经是完整路径(兼容旧数据)
- else if (data.icon.includes('/') || data.icon.includes('\\')) {
- // 尝试从路径中提取文件名
- const pathParts = data.icon.replace(/\\/g, '/').split('/');
- const fileName = pathParts[pathParts.length - 1];
- if (/^\d+\.png$/.test(fileName)) {
- iconImagePath = getIconPathByFileName(fileName);
- }
- }
- }
-
- // 确定边框颜色和粗细:选中 > 已完成 > 默认
- // 选中:蓝色边框,加粗
- // 未选中但已完成:绿色边框,加粗
- // 未选中且未完成:灰色边框,加粗
- let borderClass = 'border-gray-400';
- let borderStyle: React.CSSProperties = { borderWidth: '3px' };
- let shadowStyle = {};
-
- if (selected) {
- // 选中的节点:蓝色边框,加粗
- borderClass = 'border-blue-500 shadow-md ring-2 ring-blue-300';
- borderStyle = { borderWidth: '4px' };
- shadowStyle = { boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)' };
- } else if (isSaved) {
- // 未选中但已保存的节点:绿色边框,加粗
- borderClass = 'border-green-500';
- borderStyle = { borderWidth: '3px' };
- shadowStyle = { boxShadow: '0 0 0 1px rgba(34, 197, 94, 0.1)' };
- } else {
- // 未选中且未保存的节点:灰色边框,加粗
- borderClass = 'border-gray-400';
- borderStyle = { borderWidth: '3px' };
- shadowStyle = {};
- }
-
- return (
- <div
- className={`relative px-4 py-4 rounded-lg shadow-sm w-[180px] h-auto min-h-[140px] bg-white ${borderClass} transition-all`}
- style={{ ...borderStyle, ...shadowStyle }}
- >
- <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}
- />
-
- <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 relative`} style={{ borderRadius: '12px' }}>
- {iconImagePath ? (
- <img
- src={iconImagePath}
- alt={data.icon || '节点图标'}
- className="w-6 h-6 object-contain"
- onError={(e) => {
- // 如果图片加载失败,回退到默认图标
- console.error('节点图标加载失败:', iconImagePath);
- (e.target as HTMLImageElement).style.display = 'none';
- }}
- />
- ) : (
- <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>
- <div className="text-xs text-gray-500 text-center flex-shrink-0">
- ID: {nodeId}
- </div>
- {/* 保存状态显示 */}
- <div className="flex items-center justify-center mt-2 mb-0 flex-shrink-0">
- {isSaved ? (
- <div className="flex items-center gap-1.5 text-green-600">
- <CheckCircleOutlined style={{ fontSize: '20px', color: '#22c55e' }} />
- <span className="text-xs font-medium">已配置</span>
- </div>
- ) : (
- <div className="flex items-center gap-1.5 text-gray-400">
- <ClockCircleOutlined style={{ fontSize: '18px' }} />
- <span className="text-xs">未配置</span>
- </div>
- )}
- </div>
- </div>
- </div>
- );
- }
- // 节点类型映射(将在组件内部重新定义以访问状态)
- // 自定义边组件
- function CustomEdgeWithDelete({
- id,
- sourceX,
- sourceY,
- targetX,
- targetY,
- selected,
- markerEnd,
- markerStart,
- style,
- }: any) {
- const [edgePath] = getStraightPath({
- sourceX,
- sourceY,
- targetX,
- targetY,
- });
- return (
- <>
- <BaseEdge
- id={id}
- path={edgePath}
- markerEnd={markerEnd}
- markerStart={markerStart}
- style={{
- ...style,
- strokeWidth: selected ? 3 : 2,
- stroke: selected ? '#3b82f6' : (style?.stroke || '#000000'),
- }}
- />
- </>
- );
- }
- const edgeTypes: EdgeTypes = {
- straight: CustomEdgeWithDelete,
- default: CustomEdgeWithDelete,
- };
- export default function IsolationWork({ subMenu }: IsolationWorkProps) {
- const { t, i18n } = useTranslation();
- const navigate = useNavigate();
- const [searchTerm, setSearchTerm] = useState('');
- const [showAddModal, setShowAddModal] = useState(false);
- const [editingItem, setEditingItem] = useState<TableRow | null>(null);
- const [form] = Form.useForm();
-
- // 发起作业多步骤表单状态
- const [workJobStep, setWorkJobStep] = useState(0); // 0: 基本信息, 1: 流程管理, 2: 发布作业
- const [workJobBasicForm] = Form.useForm(); // 基本信息表单
- const [workJobPublishForm] = Form.useForm(); // 发布作业表单
- const [workflowJson, setWorkflowJson] = useState<any>(null); // 流程设计JSON
- const [workflowWorkId, setWorkflowWorkId] = useState<number | null>(null); // 作业票ID(新增后返回)
- const [workflowWorkNodeDOList, setWorkflowWorkNodeDOList] = useState<WorkflowWorkNodeDO[]>([]); // 作业节点数据列表
- const [originalBasicFormData, setOriginalBasicFormData] = useState<any>(null); // 编辑时的原始表单数据(用于判断是否有修改)
-
- // 查看详情弹框状态(只读模式)
- const [showViewModal, setShowViewModal] = useState(false);
- const [viewWorkJobStep, setViewWorkJobStep] = useState(0); // 0: 基本信息, 1: 流程管理, 2: 发布作业
- const [viewWorkJobBasicForm] = Form.useForm(); // 查看模式基本信息表单
- const [viewWorkJobPublishForm] = Form.useForm(); // 查看模式发布作业表单
- const [viewWorkflowJson, setViewWorkflowJson] = useState<any>(null); // 查看模式流程设计JSON
- const [viewWorkDetailData, setViewWorkDetailData] = useState<any>(null); // 查看模式详情数据
- const [isViewMode, setIsViewMode] = useState(false); // 是否为查看模式(只读)
-
- // ReactFlow相关状态(用于流程管理tab)
- const [workflowNodes, setWorkflowNodes, onWorkflowNodesChange] = useNodesState([]);
- const [workflowEdges, setWorkflowEdges, onWorkflowEdgesChange] = useEdgesState([]);
- const [selectedWorkflowNode, setSelectedWorkflowNode] = useState<Node | null>(null);
- const [workflowActiveTabKey, setWorkflowActiveTabKey] = useState<string>('info');
- const workflowReactFlowWrapper = useRef<HTMLDivElement>(null);
- const [workflowReactFlowInstance, setWorkflowReactFlowInstance] = useState<any>(null);
-
- // 节点配置状态
- const [workflowNodeConfig, setWorkflowNodeConfig] = useState({
- nodeName: '',
- nodeIcon: '',
- responsible: undefined as number | string | undefined,
- remark: '',
- submitForm: undefined as number | string | undefined,
- isolationType: '',
- isolationPoints: [] as string[],
- isolationNode: [] as string[],
- isolationNodeUuid: '',
- lockPerson: undefined as number | string | undefined,
- coLockPersons: [] as (number | string)[],
- notificationMethods: {
- sms: false,
- message: false,
- email: false,
- app: false,
- },
- notificationPerson: '',
- notificationTime: '',
- smsTemplateCode: 'false',
- messageTemplateCode: 'false',
- emailTemplateCode: 'false',
- appTemplateCode: 'false',
- });
-
- // 节点配置缓存(Map<nodeId, config>)
- const [workflowNodeConfigCache, setWorkflowNodeConfigCache] = useState<Map<string, any>>(new Map());
-
- // 已完成的节点ID集合
- const [completedNodeIds, setCompletedNodeIds] = useState<Set<string>>(new Set());
-
- // 节点保存状态缓存(Map<nodeId, boolean>)- 用于记录当前作业中哪些节点已保存
- const [nodeSavedStatusCache, setNodeSavedStatusCache] = useState<Map<string, boolean>>(new Map());
-
- // 计算是否有未保存的节点(用于控制流程管理tab的下一步按钮)
- const hasUnsavedNodes = useMemo(() => {
- if (workflowNodes.length === 0) {
- return false; // 如果没有节点,认为没有未保存的节点
- }
- // 检查所有节点是否都已保存
- return workflowNodes.some(node => {
- const isSaved = nodeSavedStatusCache.get(node.id) || false;
- return !isSaved;
- });
- }, [workflowNodes, nodeSavedStatusCache]);
-
- // 节点类型映射(使用 useMemo 避免每次渲染都创建新对象)
- const nodeTypes: NodeTypes = useMemo(() => ({
- createJob: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
- confirm: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
- review: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
- inputInfo: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
- isolation: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
- releaseIsolation: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
- returnLock: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
- complete: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
- }), [nodeSavedStatusCache]);
-
- // 角色用户列表和表单列表
- const [workflowDrawerUsers, setWorkflowDrawerUsers] = useState<UserVO[]>([]);
- const [workflowLockerUsers, setWorkflowLockerUsers] = useState<UserVO[]>([]);
- const [workflowColockerUsers, setWorkflowColockerUsers] = useState<UserVO[]>([]);
- const [workflowIsolationPoints, setWorkflowIsolationPoints] = useState<SegregationPointVO[]>([]);
- const [workflowFormList, setWorkflowFormList] = useState<FormVO[]>([]);
-
- // 流程设计列表数据(用于流程设计页面)
- const [processDesignList, setProcessDesignList] = useState<WorkflowDesignVO[]>([]);
- const [processDesignTotal, setProcessDesignTotal] = useState(0);
- const [processDesignLoading, setProcessDesignLoading] = useState(false);
- const [processDesignStatusUpdating, setProcessDesignStatusUpdating] = useState<Record<number, boolean>>({});
-
- // 作业分类字典数据
- const [workTypeDictList, setWorkTypeDictList] = useState<DictDataVO[]>([]);
-
- // 紧急程度字典数据
- const [urgencyLevelDictList, setUrgencyLevelDictList] = useState<DictDataVO[]>([]);
-
- // 隔离方式字典数据
- const [isolationTypeDictList, setIsolationTypeDictList] = useState<DictDataVO[]>([]);
- const [jobStatusDictList, setJobStatusDictList] = useState<DictDataVO[]>([]);
-
- // 流程设计列表数据(用于流程模板下拉框)
- const [workflowTemplateList, setWorkflowTemplateList] = useState<WorkflowDesignVO[]>([]);
- // 作业管理列表数据
- const [workJobList, setWorkJobList] = useState<WorkJobVO[]>([]);
- const [workJobTotal, setWorkJobTotal] = useState(0);
- const [workJobLoading, setWorkJobLoading] = useState(false);
- // 流程模板数据
- const templateData: TableRow[] = [
- {
- id: 1,
- code: 'TPL-001',
- name: '高压设备隔离流程',
- type: '标准流程',
- category: '高压作业',
- steps: 8,
- approvalLevel: '二级审批',
- creator: '张三',
- createTime: '2024-01-10',
- status: '启用',
- useCount: 45,
- remark: '适用于10kV及以上高压设备'
- },
- {
- id: 2,
- code: 'TPL-002',
- name: '低压配电柜隔离流程',
- type: '标准流程',
- category: '低压作业',
- steps: 6,
- approvalLevel: '一级审批',
- creator: '李四',
- createTime: '2024-01-15',
- status: '启用',
- useCount: 128,
- remark: '适用于380V配电柜'
- },
- {
- id: 3,
- code: 'TPL-003',
- name: '紧急抢修隔离流程',
- type: '应急流程',
- category: '应急处理',
- steps: 5,
- approvalLevel: '特殊审批',
- creator: '王五',
- createTime: '2024-02-01',
- status: '启用',
- useCount: 12,
- remark: '用于紧急故障抢修'
- },
- {
- id: 4,
- code: 'TPL-004',
- name: '定期维护隔离流程',
- type: '标准流程',
- category: '定期维护',
- steps: 7,
- approvalLevel: '一级审批',
- creator: '张三',
- createTime: '2024-02-10',
- status: '启用',
- useCount: 67,
- remark: '用于定期维护保养'
- },
- ];
- // SOP管理数据
- const sopData: TableRow[] = [
- {
- id: 1,
- code: 'SOP-001',
- name: '高压开关操作规程',
- version: 'V2.1',
- category: '操作规程',
- department: '技术部',
- effectiveDate: '2024-01-01',
- reviewDate: '2025-01-01',
- status: '有效',
- reviewer: '李四',
- lastUpdate: '2024-12-01',
- attachments: 2,
- remark: '包含视频培训资料'
- },
- {
- id: 2,
- code: 'SOP-002',
- name: '配电柜断电操作规程',
- version: 'V1.5',
- category: '操作规程',
- department: '运维部',
- effectiveDate: '2024-02-01',
- reviewDate: '2025-02-01',
- status: '有效',
- reviewer: '张三',
- lastUpdate: '2024-11-15',
- attachments: 1,
- remark: ''
- },
- {
- id: 3,
- code: 'SOP-003',
- name: '接地线安装规程',
- version: 'V3.0',
- category: '安全规程',
- department: '安全部',
- effectiveDate: '2024-01-15',
- reviewDate: '2025-01-15',
- status: '有效',
- reviewer: '王五',
- lastUpdate: '2024-12-03',
- attachments: 3,
- remark: '新增便携式接地线章节'
- },
- {
- id: 4,
- code: 'SOP-004',
- name: '验电作业规程',
- version: 'V2.0',
- category: '安全规程',
- department: '安全部',
- effectiveDate: '2024-03-01',
- reviewDate: '2025-03-01',
- status: '待审核',
- reviewer: '赵六',
- lastUpdate: '2024-11-30',
- attachments: 1,
- remark: '待技术部审核'
- },
- ];
- // 表单管理数据
- const formManagementData: TableRow[] = [
- {
- id: 1,
- name: '高压作业申请表',
- creator: '张三',
- createTime: '2024-01-10 14:30',
- version: 'V1.0',
- status: '启用',
- description: '用于高压设备作业申请的表单',
- useCount: 45
- },
- {
- id: 2,
- name: '低压作业申请表',
- creator: '李四',
- createTime: '2024-01-15 09:20',
- version: 'V1.2',
- status: '启用',
- description: '用于低压设备作业申请的表单',
- useCount: 128
- },
- {
- id: 3,
- name: '紧急抢修申请表',
- creator: '王五',
- createTime: '2024-02-01 16:45',
- version: 'V2.0',
- status: '启用',
- description: '用于紧急故障抢修申请的表单',
- useCount: 12
- },
- {
- id: 4,
- name: '定期维护申请表',
- creator: '张三',
- createTime: '2024-02-10 11:15',
- version: 'V1.5',
- status: '停用',
- description: '用于定期维护申请的表单',
- useCount: 67
- },
- ];
- // 表单管理搜索参数
- const [formManagementQuery, setFormManagementQuery] = useState({
- name: '',
- status: undefined as string | undefined,
- });
- const [formManagementPagination, setFormManagementPagination] = useState({
- pageNo: 1,
- pageSize: 10,
- });
- // 流程设计数据
- const processDesignData: TableRow[] = [
- {
- id: 1,
- name: '高压设备隔离流程设计',
- designer: '张三',
- designTime: '2024-01-10 14:30',
- nodeCount: 8,
- description: '适用于10kV及以上高压设备的隔离作业流程',
- status: '启用'
- },
- {
- id: 2,
- name: '低压配电柜隔离流程设计',
- designer: '李四',
- designTime: '2024-01-15 09:20',
- nodeCount: 6,
- description: '适用于380V配电柜的隔离作业流程',
- status: '启用'
- },
- {
- id: 3,
- name: '紧急抢修隔离流程设计',
- designer: '王五',
- designTime: '2024-02-01 16:45',
- nodeCount: 5,
- description: '用于紧急故障抢修的快速隔离流程',
- status: '启用'
- },
- {
- id: 4,
- name: '定期维护隔离流程设计',
- designer: '张三',
- designTime: '2024-02-10 11:15',
- nodeCount: 7,
- description: '用于定期维护保养的隔离流程',
- status: '停用'
- },
- ];
- // 流程设计搜索参数
- const [processDesignQuery, setProcessDesignQuery] = useState({
- name: '',
- });
- const [processDesignPagination, setProcessDesignPagination] = useState({
- pageNo: 1,
- pageSize: 10,
- });
- // 作业管理搜索参数
- // 从 sessionStorage 读取初始 status
- const getInitialWorkStatus = () => {
- const status = sessionStorage.getItem('workManagementStatus');
- return status || undefined;
- };
- const [workJobQuery, setWorkJobQuery] = useState({
- name: '',
- status: getInitialWorkStatus() as string | undefined,
- });
- const [workJobPagination, setWorkJobPagination] = useState({
- pageNo: 1,
- pageSize: 10,
- });
- // 作业管理数据(旧数据,保留用于兼容)
- const workData: TableRow[] = [
- {
- id: 1,
- code: 'WORK-2025-001',
- name: '1号变压器定期检修',
- type: '定期维护',
- template: '定期维护隔离流程',
- location: 'B区地下室变压器房',
- equipment: '变压器柜A',
- applicant: '李四',
- department: '技术部',
- planStartTime: '2025-12-10 08:00',
- planEndTime: '2025-12-10 18:00',
- status: '待审批',
- approver: '王五',
- priority: '普通',
- remark: '年度例行检修'
- },
- {
- id: 2,
- code: 'WORK-2025-002',
- name: '2号配电柜故障抢修',
- type: '应急处理',
- template: '紧急抢修隔离流程',
- location: 'A区2层配电室',
- equipment: '2号配电柜',
- applicant: '张三',
- department: '运维部',
- planStartTime: '2025-12-04 14:00',
- planEndTime: '2025-12-04 18:00',
- status: '进行中',
- approver: '李四',
- priority: '紧急',
- remark: '断路器跳闸需紧急处理'
- },
- {
- id: 3,
- code: 'WORK-2025-003',
- name: 'A区照明回路维护',
- type: '定期维护',
- template: '低压配电柜隔离流程',
- location: 'A区1层配电室',
- equipment: '1号配电柜',
- applicant: '王五',
- department: '运维部',
- planStartTime: '2025-12-05 09:00',
- planEndTime: '2025-12-05 12:00',
- status: '已完成',
- approver: '张三',
- priority: '普通',
- remark: ''
- },
- {
- id: 4,
- code: 'WORK-2025-004',
- name: 'C区空调系统检查',
- type: '定期维护',
- template: '低压配电柜隔离流程',
- location: 'C区3层控制室',
- equipment: '控制柜C1',
- applicant: '赵六',
- department: '运维部',
- planStartTime: '2025-12-06 08:00',
- planEndTime: '2025-12-06 11:00',
- status: '已完成',
- approver: '张三',
- priority: '普通',
- remark: '冬季例行检查'
- },
- {
- id: 5,
- code: 'WORK-2025-005',
- name: '高压开关柜年检',
- type: '定期维护',
- template: '高压设备隔离流程',
- location: 'B区地下室变压器房',
- equipment: '变压器柜A',
- applicant: '李四',
- department: '技术部',
- planStartTime: '2025-12-15 08:00',
- planEndTime: '2025-12-15 17:00',
- status: '待审批',
- approver: '王五',
- priority: '重要',
- remark: '需第三方检测机构配合'
- },
- ];
- // 表单管理搜索处理
- const handleFormManagementQuery = () => {
- // 这里可以添加搜索逻辑
- console.log('搜索表单管理:', formManagementQuery);
- };
- const resetFormManagementQuery = () => {
- setFormManagementQuery({
- name: '',
- status: undefined,
- });
- };
- // 获取流程设计列表
- const getProcessDesignList = async () => {
- setProcessDesignLoading(true);
- try {
- const response = await workflowDesignApi.getWorkflowDesignPage({
- pageNo: processDesignPagination.pageNo,
- pageSize: processDesignPagination.pageSize,
- name: processDesignQuery.name || undefined,
- });
- setProcessDesignList(response.list || []);
- setProcessDesignTotal(response.total || 0);
- } catch (error: any) {
- console.error('获取流程设计列表失败:', error);
- message.error(error?.message || '获取流程设计列表失败');
- } finally {
- setProcessDesignLoading(false);
- }
- };
-
- // 获取流程设计列表(用于流程模板下拉框,获取全部数据)
- const getWorkflowTemplateList = async () => {
- try {
- const response = await workflowDesignApi.getWorkflowDesignPage({
- pageNo: 1,
- pageSize: -1,
- status: 1, // 只获取启用状态(status=1)的流程模板
- });
- setWorkflowTemplateList(response.list || []);
- } catch (error: any) {
- console.error('获取流程模板列表失败:', error);
- setWorkflowTemplateList([]);
- }
- };
- // 处理流程设计状态切换
- const handleProcessDesignStatusChanged = async (record: WorkflowDesignVO, newStatus: number) => {
- if (!record.id) {
- message.error('流程设计ID不存在');
- return;
- }
- // 设置该记录为更新中状态
- setProcessDesignStatusUpdating(prev => ({ ...prev, [record.id!]: true }));
- try {
- await workflowDesignApi.updateWorkflowDesignStatus({
- id: record.id,
- status: newStatus,
- });
-
- // 更新本地列表中的状态
- setProcessDesignList(prev =>
- prev.map(item =>
- item.id === record.id ? { ...item, status: newStatus } : item
- )
- );
-
- message.success(newStatus === 1 ? '已启用' : '已禁用');
- } catch (error: any) {
- console.error('更新流程设计状态失败:', error);
- message.error(error?.message || '更新状态失败');
- } finally {
- // 清除更新中状态
- setProcessDesignStatusUpdating(prev => {
- const newState = { ...prev };
- delete newState[record.id!];
- return newState;
- });
- }
- };
-
- // 获取作业分类字典数据
- const getWorkTypeDictList = async () => {
- try {
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'work_type',
- });
- console.log('作业分类字典API响应:', response);
- const data = (response as any)?.data || response;
- console.log('作业分类字典解析后的data:', data);
- const list = data?.list || [];
- console.log('作业分类字典数据list:', list);
- setWorkTypeDictList(list);
- } catch (error: any) {
- console.error('获取作业分类字典失败:', error);
- console.error('错误详情:', error?.response?.data || error);
- setWorkTypeDictList([]);
- }
- };
-
- // 获取紧急程度字典数据
- const getUrgencyLevelDictList = async () => {
- try {
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'urgency_level',
- });
- console.log('紧急程度字典API响应:', response);
- const data = (response as any)?.data || response;
- console.log('紧急程度字典解析后的data:', data);
- const list = data?.list || [];
- console.log('紧急程度字典数据list:', list);
- console.log('紧急程度字典数据list长度:', list.length);
- setUrgencyLevelDictList(list);
- // 如果有数据且表单还没有设置值,设置默认值为第一项
- if (list.length > 0 && !workJobBasicForm.getFieldValue('urgency')) {
- workJobBasicForm.setFieldsValue({ urgency: list[0].value });
- }
- } catch (error: any) {
- console.error('获取紧急程度字典失败:', error);
- console.error('错误详情:', error?.response?.data || error);
- setUrgencyLevelDictList([]);
- }
- };
-
- // 获取隔离方式字典数据
- 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([]);
- }
- };
- // 获取作业状态字典数据
- const getJobStatusDictList = async () => {
- try {
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'job_status',
- });
- const data = (response as any)?.data || response;
- const list = data?.list || [];
- setJobStatusDictList(list);
- } catch (error: any) {
- console.error('获取作业状态字典失败:', error);
- setJobStatusDictList([]);
- }
- };
- // 流程设计搜索处理
- const handleProcessDesignQuery = () => {
- setProcessDesignPagination({ ...processDesignPagination, pageNo: 1 });
- };
- const resetProcessDesignQuery = () => {
- setProcessDesignQuery({
- name: '',
- });
- setProcessDesignPagination({ pageNo: 1, pageSize: 10 });
- // 立即调用接口刷新数据
- getProcessDesignList();
- };
- // 监听菜单切换,重置状态并加载数据
- useEffect(() => {
- // 当切换菜单时,重置相关状态并加载数据
- if (subMenu === '流程设计') {
- // 重置分页到第一页
- setProcessDesignPagination({ pageNo: 1, pageSize: 10 });
- // 重置查询条件
- setProcessDesignQuery({ name: '' });
- // 加载数据
- getProcessDesignList();
- } else if (subMenu === '作业管理') {
- // 重置分页到第一页
- setWorkJobPagination({ pageNo: 1, pageSize: 10 });
- // 检查是否有从 sessionStorage 传入的 status
- const statusFromStorage = sessionStorage.getItem('workManagementStatus');
- // 重置查询条件(如果有 status 则保留,否则清空)
- setWorkJobQuery({ name: '', status: statusFromStorage || undefined });
- // 如果读取了 status,清除 sessionStorage
- if (statusFromStorage) {
- sessionStorage.removeItem('workManagementStatus');
- }
- // 加载数据
- getWorkJobList();
- // 加载作业分类字典数据
- getWorkTypeDictList();
- // 加载紧急程度字典数据
- getUrgencyLevelDictList();
- // 加载作业状态字典数据
- getJobStatusDictList();
- // 加载隔离方式字典数据
- getIsolationMethodDictList();
- // 加载流程设计列表(用于流程模板下拉框)
- getWorkflowTemplateList();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [subMenu]);
- // 流程设计页面加载时获取列表
- useEffect(() => {
- if (subMenu === '流程设计') {
- getProcessDesignList();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [processDesignPagination.pageNo, processDesignPagination.pageSize, processDesignQuery.name]);
- // 获取作业管理列表
- const getWorkJobList = async () => {
- setWorkJobLoading(true);
- try {
- const response = await workJobApi.getWorkflowWorkPage({
- pageNo: workJobPagination.pageNo,
- pageSize: workJobPagination.pageSize,
- name: workJobQuery.name || undefined,
- status: workJobQuery.status || undefined,
- });
- setWorkJobList(response.list || []);
- setWorkJobTotal(response.total || 0);
- } catch (error: any) {
- console.error('获取作业列表失败:', error);
- message.error(error?.message || '获取作业列表失败');
- setWorkJobList([]);
- setWorkJobTotal(0);
- } finally {
- setWorkJobLoading(false);
- }
- };
- // 作业管理搜索处理
- const handleWorkJobQuery = () => {
- setWorkJobPagination({ ...workJobPagination, pageNo: 1 });
- };
- const resetWorkJobQuery = () => {
- setWorkJobQuery({
- name: '',
- status: undefined,
- });
- setWorkJobPagination({ pageNo: 1, pageSize: 10 });
- };
- // 组件挂载时从 sessionStorage 读取 status
- useEffect(() => {
- const status = sessionStorage.getItem('workManagementStatus');
- if (status) {
- console.log('从 sessionStorage 读取到 workManagementStatus:', status);
- setWorkJobQuery(prev => ({
- ...prev,
- status: status,
- }));
- // 读取后清除 sessionStorage,避免下次进入时自动应用
- sessionStorage.removeItem('workManagementStatus');
- }
- }, []);
- // 作业管理页面加载时获取列表
- useEffect(() => {
- if (subMenu === '作业管理') {
- getWorkJobList();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [workJobPagination.pageNo, workJobPagination.pageSize, workJobQuery.name, workJobQuery.status]);
- // 加载流程设计JSON并渲染到ReactFlow
- const loadWorkflowJson = async (workflowId: number) => {
- try {
- const response = await workflowDesignApi.selectWorkflowDesignById(workflowId);
- console.log('流程设计详情API响应:', response);
-
- // axios拦截器可能已经处理了数据格式,直接使用response
- const workflow = response;
- console.log('流程设计数据:', workflow);
- // console.log('content字段:', workflow.content);
-
- // 流程设计数据在 content 字段中(JSON字符串)
- let jsonData: any = null;
- // 检查 content 是否为 null 或空字符串
- const hasContent = workflow.content && workflow.content !== null && workflow.content !== '';
-
- if (hasContent) {
- try {
- jsonData = typeof workflow.content === 'string'
- ? JSON.parse(workflow.content)
- : workflow.content;
- console.log('解析后的JSON数据:', jsonData);
- setWorkflowJson(jsonData);
- } catch (parseError) {
- console.error('解析JSON失败:', parseError);
- console.error('原始content:', workflow.content);
- message.error('流程设计JSON格式错误');
- // 解析失败时清空流程
- setWorkflowJson(null);
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- setNodeSavedStatusCache(new Map());
- setSelectedWorkflowNode(null);
- return;
- }
- } else {
- // content 为 null 或空时,清空流程,不使用备用数据
- console.warn('流程设计content为空或null,清空流程');
- setWorkflowJson(null);
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- setNodeSavedStatusCache(new Map());
- setSelectedWorkflowNode(null);
- return;
- }
-
- // 解析并渲染到ReactFlow
- if (jsonData && jsonData.nodes && Array.isArray(jsonData.nodes) && jsonData.edges && Array.isArray(jsonData.edges)) {
- console.log('开始渲染节点和连线,节点数量:', jsonData.nodes.length, '连线数量:', jsonData.edges.length);
-
- // 创建节点映射,方便后续查找已保存的节点
- const nodeDOMap = new Map<string, WorkflowWorkNodeDO>();
- workflowWorkNodeDOList.forEach((nodeDO: WorkflowWorkNodeDO) => {
- if (nodeDO.uuid) {
- nodeDOMap.set(nodeDO.uuid, nodeDO);
- }
- });
-
- const importedNodes: Node[] = jsonData.nodes.map((node: any) => {
- const nodeData = node.data || {};
- const nodeId = node.id || node.uuid;
- const nodeDO = nodeDOMap.get(nodeId);
-
- // 获取图标:优先使用 nodeIcon(顶层),其次使用 data.icon
- const iconValue = node.nodeIcon || nodeData.icon || '';
-
- return {
- id: nodeId,
- type: node.type || 'createJob',
- position: node.position || { x: 0, y: 0 },
- data: (() => {
- const { isolationMethod, ...restNodeData } = nodeData || {};
- return {
- ...restNodeData,
- label: nodeData.label || node.label || node.nodeName || nodeConfigs.find(c => c.type === (node.type || nodeData.type))?.label || '节点',
- type: node.type || nodeData.type || 'createJob',
- icon: iconValue, // 确保icon字段被正确传递
- // 标记节点是否已完成配置(如果 nodeDO 存在且有 id,说明已经保存过)
- completed: !!nodeDO?.id,
- };
- })(),
- };
- });
-
- const importedEdges: Edge[] = jsonData.edges.map((edge: any) => {
- // 处理sourceHandle和targetHandle
- // 如果JSON中有指定handle,使用指定的;否则根据连接方向推断
- let sourceHandle = edge.sourceHandle;
- let targetHandle = edge.targetHandle;
-
- // 如果没有指定handle,尝试从edge的id或其他字段推断
- // 例如:edge id可能是 "edge-confirm-xxx-right-source-createJob-xxx-left-target-xxx"
- if (!sourceHandle && edge.id) {
- const sourceMatch = edge.id.match(/right-source|left-source|top-source|bottom-source/);
- if (sourceMatch) {
- sourceHandle = sourceMatch[0].replace('-source', '');
- } else {
- // 默认使用右侧连接点
- sourceHandle = 'right-source';
- }
- } else if (!sourceHandle) {
- // 默认使用右侧连接点
- sourceHandle = 'right-source';
- }
-
- if (!targetHandle && edge.id) {
- const targetMatch = edge.id.match(/right-target|left-target|top-target|bottom-target/);
- if (targetMatch) {
- targetHandle = targetMatch[0].replace('-target', '');
- } else {
- // 默认使用左侧连接点
- targetHandle = 'left-target';
- }
- } else if (!targetHandle) {
- // 默认使用左侧连接点
- targetHandle = 'left-target';
- }
-
- return {
- id: edge.id || `${edge.source}-${edge.target}`,
- source: edge.source,
- target: edge.target,
- sourceHandle: sourceHandle,
- targetHandle: targetHandle,
- type: 'straight',
- style: { strokeWidth: 2, stroke: '#000000' },
- markerStart: {
- type: 'arrowclosed',
- color: '#000000',
- },
- };
- });
-
- console.log('导入的节点:', importedNodes);
- console.log('导入的连线:', importedEdges);
-
- setWorkflowNodes(importedNodes);
- setWorkflowEdges(importedEdges);
-
- // 清空之前的缓存和完成状态
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- // 清除节点保存状态缓存
- setNodeSavedStatusCache(new Map());
-
- // 自动选中第一个节点
- if (importedNodes.length > 0) {
- const firstNode = importedNodes[0];
- setTimeout(() => {
- setSelectedWorkflowNode(firstNode);
- const source = firstNode.data || {};
- const config = nodeConfigs.find(c => c.type === source.type);
- setWorkflowNodeConfig({
- nodeName: source.label || config?.label || '',
- nodeIcon: source.icon || '',
- responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
- remark: source.remark || '',
- submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
- isolationType: source.isolationType || '',
- isolationPoints: source.isolationPoints || [],
- isolationNode: source.isolationNode || [],
- isolationNodeUuid: source.isolationNodeUuid || '',
- lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
- coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
- notificationMethods: source.notificationMethods || {
- sms: false,
- message: false,
- email: false,
- app: false,
- },
- notificationPerson: source.notificationPerson || '',
- notificationTime: source.notificationTime || '',
- });
- // 高亮第一个节点
- setWorkflowNodes((nds) =>
- nds.map((node) =>
- node.id === firstNode.id
- ? { ...node, selected: true }
- : { ...node, selected: false }
- )
- );
- }, 200);
- }
- } else {
- console.error('JSON数据格式错误,缺少nodes或edges:', jsonData);
- message.error('流程设计数据格式错误:缺少节点或连线数据');
- }
- } catch (error: any) {
- console.error('加载流程设计失败:', error);
- console.error('错误详情:', error?.response?.data || error);
- message.error('加载流程设计失败: ' + (error?.message || '未知错误'));
-
- // 使用备用JSON
- const fallbackJson = {
- nodes: [
- { id: '1', type: 'createJob', label: '提交/开始', position: { x: 100, y: 100 } },
- { id: '2', type: 'review', label: '审核/确认', position: { x: 300, y: 100 } },
- { id: '3', type: 'inputInfo', label: '录入/表单', position: { x: 500, y: 100 } },
- { id: '4', type: 'isolation', label: '隔离/方案', position: { x: 700, y: 100 } },
- { id: '5', type: 'releaseIsolation', label: '取锁/共锁', position: { x: 700, y: 300 } },
- { id: '6', type: 'complete', label: '完成/结束', position: { x: 900, y: 300 } },
- ],
- edges: [
- { id: 'e1-2', source: '1', target: '2' },
- { id: 'e2-3', source: '2', target: '3' },
- { id: 'e3-4', source: '3', target: '4' },
- { id: 'e4-5', source: '4', target: '5' },
- { id: 'e5-6', source: '5', target: '6' },
- { id: 'e2-5', source: '2', target: '5' },
- ],
- };
- setWorkflowJson(fallbackJson);
-
- const importedNodes: Node[] = fallbackJson.nodes.map((node: any) => ({
- id: node.id,
- type: node.type,
- position: node.position,
- data: {
- label: node.label,
- type: node.type,
- icon: node.icon || node.nodeIcon || '', // 确保icon字段被传递
- },
- }));
-
- const importedEdges: Edge[] = fallbackJson.edges.map((edge: any) => ({
- id: edge.id,
- source: edge.source,
- target: edge.target,
- type: 'straight',
- style: { strokeWidth: 2, stroke: '#000000' },
- markerStart: {
- type: 'arrowclosed',
- color: '#000000',
- },
- }));
-
- setWorkflowNodes(importedNodes);
- setWorkflowEdges(importedEdges);
- }
- };
-
- // 加载角色用户列表和表单列表(用于节点配置)
- useEffect(() => {
- if (workJobStep === 1) {
- const loadRoleUsers = async () => {
- try {
- const [drawerRes, lockerRes, colockerRes] = await Promise.all([
- userApi.getRoleUser('jtdrawer'),
- userApi.getRoleUser('jtlocker'),
- userApi.getRoleUser('jtcolocker'),
- ]);
- setWorkflowDrawerUsers(drawerRes || []);
- setWorkflowLockerUsers(lockerRes || []);
- setWorkflowColockerUsers(colockerRes || []);
- } catch (error) {
- console.error('加载角色用户失败:', error);
- }
- };
-
- const loadIsolationPoints = async () => {
- try {
- const res = await segregationPointApi.getIsIsolationPointPage({ pageNo: 1, pageSize: -1 });
- setWorkflowIsolationPoints(res.list || []);
- } catch (error) {
- console.error('加载隔离点列表失败:', error);
- }
- };
-
- const loadFormList = async () => {
- try {
- // 只获取开启状态(status=0)的表单
- const res = await getFormPage({ pageNo: 1, pageSize: -1, status: 0 });
- setWorkflowFormList(res.list || []);
- } catch (error) {
- console.error('加载表单列表失败:', error);
- }
- };
-
- // 初始化节点状态:调用 checkWorkById 和 selectWorkflowWorkById 接口
- const initializeNodeStates = async () => {
- if (!workflowWorkId) {
- console.warn('workflowWorkId 不存在,无法初始化节点状态');
- return;
- }
-
- try {
- // 1. 调用 checkWorkById 获取未保存的节点列表
- const checkResponse = await workJobApi.checkWorkById(workflowWorkId);
- const checkData = (checkResponse as any)?.data || checkResponse;
- const unsavedNodeMessages = Array.isArray(checkData) ? checkData : (checkData ? [checkData] : []);
-
- // 2. 调用 selectWorkflowWorkById 获取所有节点数据
- const detailResponse = await workJobApi.selectWorkflowWorkById(workflowWorkId);
- const detail = detailResponse as any;
-
- if (detail.workflowWorkNodeDOList && Array.isArray(detail.workflowWorkNodeDOList)) {
- // 更新 workflowWorkNodeDOList
- setWorkflowWorkNodeDOList(detail.workflowWorkNodeDOList);
-
- // 3. 根据接口数据初始化缓存和节点状态
- // 创建新的缓存和状态,不使用旧的(因为我们要用接口数据覆盖)
- const newCache = new Map<string, any>();
- const newCompleted = new Set<string>();
- const newSavedStatusCache = new Map<string, boolean>();
-
- // 遍历所有节点,初始化缓存
- detail.workflowWorkNodeDOList.forEach((nodeDO: WorkflowWorkNodeDO) => {
- if (!nodeDO.uuid) return;
-
- // 解析节点数据
- let nodeData: any = {};
- if (nodeDO.data) {
- try {
- nodeData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
- } catch (e) {
- console.error('解析节点data失败:', e);
- }
- }
-
- // 构建节点配置
- const config = nodeConfigs.find(c => c.type === (nodeDO.type || nodeData.type));
- const nodeConfig = {
- nodeName: nodeDO.nodeName || nodeData.label || config?.label || '',
- nodeIcon: nodeDO.nodeIcon || nodeData.icon || '',
- responsible: (nodeDO.workerUserId !== null && nodeDO.workerUserId !== undefined && nodeDO.workerUserId !== 0)
- ? (typeof nodeDO.workerUserId === 'number' ? nodeDO.workerUserId : Number(nodeDO.workerUserId))
- : (nodeData.workerUserId && nodeData.workerUserId !== '' && nodeData.workerUserId !== '0')
- ? (typeof nodeData.workerUserId === 'number' ? nodeData.workerUserId : Number(nodeData.workerUserId))
- : undefined,
- remark: nodeData.remark || '',
- submitForm: nodeDO.formId ? (typeof nodeDO.formId === 'number' ? nodeDO.formId : Number(nodeDO.formId)) : (nodeData.submitForm ? (typeof nodeData.submitForm === 'number' ? nodeData.submitForm : Number(nodeData.submitForm)) : undefined),
- isolationType: (nodeDO.isolationType !== null && nodeDO.isolationType !== undefined) ? String(nodeDO.isolationType) : (nodeData.isolationType || ''),
- isolationPoints: nodeDO.isolationPoints ? (typeof nodeDO.isolationPoints === 'string' ? JSON.parse(nodeDO.isolationPoints) : nodeDO.isolationPoints) : (nodeData.isolationPoints || []),
- isolationNode: nodeData.isolationNode || [],
- isolationNodeUuid: nodeDO.isolationNodeUuid || nodeData.isolationNodeUuid || '',
- lockPerson: (() => {
- // 优先从 nodeUserList 中提取
- let lockPersonId: number | undefined = undefined;
- if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
- const lockerUser = nodeDO.nodeUserList.find((user: any) => user.type === 'jtlocker');
- if (lockerUser?.userId) {
- lockPersonId = typeof lockerUser.userId === 'number' ? lockerUser.userId : Number(lockerUser.userId);
- }
- }
- return lockPersonId || (nodeData.lockPerson ? (typeof nodeData.lockPerson === 'number' ? nodeData.lockPerson : Number(nodeData.lockPerson)) : undefined);
- })(),
- coLockPersons: (() => {
- // 优先从 nodeUserList 中提取
- const coLockPersonIds: number[] = [];
- if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
- nodeDO.nodeUserList.forEach((user: any) => {
- if (user.type === 'jtcolocker' && user.userId) {
- coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
- }
- });
- }
- return coLockPersonIds.length > 0 ? coLockPersonIds : (nodeData.coLockPersons && Array.isArray(nodeData.coLockPersons) ? nodeData.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
- })(),
- notificationMethods: nodeData.notificationMethods || {
- sms: false,
- message: false,
- email: false,
- app: false,
- },
- notificationPerson: nodeData.notificationPerson || '',
- notificationTime: nodeDO.notifyTime || nodeData.notificationTime || '',
- smsTemplateCode: nodeData.smsTemplateCode || (nodeData.notificationMethods?.sms ? 'true' : 'false'),
- messageTemplateCode: nodeData.messageTemplateCode || (nodeData.notificationMethods?.message ? 'true' : 'false'),
- emailTemplateCode: nodeData.emailTemplateCode || (nodeData.notificationMethods?.email ? 'true' : 'false'),
- appTemplateCode: nodeData.appTemplateCode || (nodeData.notificationMethods?.app ? 'true' : 'false'),
- };
-
- // 将节点配置存入缓存
- newCache.set(nodeDO.uuid, nodeConfig);
-
- // 检查节点是否已保存(如果不在未保存列表中,说明已保存)
- // 通过检查未保存消息中是否包含该节点的信息来判断
- const isNodeUnsaved = unsavedNodeMessages.some((msg: string) => {
- // 如果消息中包含节点名称或UUID,说明该节点未保存
- return msg.includes(nodeDO.nodeName || '') || msg.includes(nodeDO.uuid || '');
- });
-
- if (!isNodeUnsaved) {
- // 节点已保存,标记为已完成
- newCompleted.add(nodeDO.uuid);
- newSavedStatusCache.set(nodeDO.uuid, true);
- } else {
- // 节点未保存
- newSavedStatusCache.set(nodeDO.uuid, false);
- }
- });
-
- // 更新缓存和状态
- setWorkflowNodeConfigCache(newCache);
- setCompletedNodeIds(newCompleted);
- setNodeSavedStatusCache(newSavedStatusCache);
-
- console.log('节点状态初始化完成:', {
- totalNodes: detail.workflowWorkNodeDOList.length,
- completedNodes: newCompleted.size,
- unsavedNodes: unsavedNodeMessages.length,
- });
-
- // 找到第一个未保存的节点并自动选中(按照流程顺序)
- // 等待 workflowNodes 渲染完成后再选中
- // 使用 setState 的回调形式确保获取最新的 workflowNodes
- setTimeout(() => {
- setWorkflowNodes((currentNodes) => {
- // 使用 getNextIncompleteNode 的逻辑,但基于 nodeSavedStatusCache 来判断
- // 找到第一个未保存的节点(按照流程顺序)
- let firstUnsavedNode: Node | null = null;
-
- if (workflowJson && workflowJson.adjacency && currentNodes.length > 0) {
- const adjacency = workflowJson.adjacency;
- const nodeMap = new Map<string, Node>();
-
- // 初始化节点映射
- currentNodes.forEach(node => {
- nodeMap.set(node.id, node);
- });
-
- // 辅助函数:检查节点的所有父节点是否都已保存
- const areAllParentsSaved = (nodeId: string): boolean => {
- const nodeInfo = adjacency[nodeId];
- if (!nodeInfo || !nodeInfo.parentUuid || nodeInfo.parentUuid.length === 0) {
- return true; // 没有父节点,可以执行
- }
- return nodeInfo.parentUuid.every((parentId: string) => {
- const isParentSaved = newSavedStatusCache.get(parentId);
- return isParentSaved === true; // 父节点已保存
- });
- };
-
- // 辅助函数:获取节点的第一个未保存的子节点
- const getFirstUnsavedChild = (nodeId: string): string | null => {
- const nodeInfo = adjacency[nodeId];
- if (!nodeInfo || !nodeInfo.childrenUuid || nodeInfo.childrenUuid.length === 0) {
- return null; // 没有子节点
- }
-
- // 按顺序查找第一个未保存的子节点
- for (const childId of nodeInfo.childrenUuid) {
- const isChildSaved = newSavedStatusCache.get(childId);
- if (isChildSaved === false && areAllParentsSaved(childId)) {
- return childId;
- }
- }
-
- return null;
- };
-
- // BFS遍历查找第一个未保存的节点
- const queue: string[] = [];
- const visited = new Set<string>();
-
- // 找到所有开始节点(parentUuid 为空的节点)
- for (const nodeId in adjacency) {
- const nodeInfo = adjacency[nodeId];
- if (!nodeInfo.parentUuid || nodeInfo.parentUuid.length === 0) {
- const isSaved = newSavedStatusCache.get(nodeId);
- if (isSaved === false) {
- // 如果开始节点未保存,直接返回
- firstUnsavedNode = nodeMap.get(nodeId) || null;
- if (firstUnsavedNode) {
- break;
- }
- } else {
- // 如果开始节点已保存,将其子节点加入队列
- if (nodeInfo.childrenUuid && nodeInfo.childrenUuid.length > 0) {
- nodeInfo.childrenUuid.forEach((childId: string) => {
- if (!visited.has(childId)) {
- queue.push(childId);
- visited.add(childId);
- }
- });
- }
- }
- }
- }
-
- // 如果开始节点都已保存,BFS遍历查找下一个未保存的节点
- if (!firstUnsavedNode) {
- while (queue.length > 0) {
- const currentNodeId = queue.shift()!;
-
- const isSaved = newSavedStatusCache.get(currentNodeId);
- if (isSaved === false && areAllParentsSaved(currentNodeId)) {
- firstUnsavedNode = nodeMap.get(currentNodeId) || null;
- if (firstUnsavedNode) {
- break;
- }
- }
-
- // 如果当前节点已保存,将其子节点加入队列
- const nodeInfo = adjacency[currentNodeId];
- if (nodeInfo && nodeInfo.childrenUuid && nodeInfo.childrenUuid.length > 0) {
- nodeInfo.childrenUuid.forEach((childId: string) => {
- if (!visited.has(childId)) {
- queue.push(childId);
- visited.add(childId);
- }
- });
- }
- }
- }
- } else {
- // 如果没有 adjacency 信息,回退到简单查找
- firstUnsavedNode = currentNodes.find(node => {
- const isSaved = newSavedStatusCache.get(node.id);
- return isSaved === false; // 明确为 false 表示未保存
- }) || null;
- }
-
- if (!firstUnsavedNode) {
- console.log('没有找到未保存的节点,所有节点都已保存');
- return currentNodes;
- }
-
- // 选中第一个未保存的节点
- setSelectedWorkflowNode(firstUnsavedNode);
-
- // 从缓存中获取节点配置
- const cachedConfig = newCache.get(firstUnsavedNode.id);
- if (cachedConfig) {
- setWorkflowNodeConfig(cachedConfig);
- } else {
- // 如果缓存中没有,从 nodeDO 中获取
- const nodeDO = detail.workflowWorkNodeDOList.find(item => item.uuid === firstUnsavedNode.id);
- if (nodeDO) {
- let nodeData: any = {};
- if (nodeDO.data) {
- try {
- nodeData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
- } catch (e) {
- console.error('解析节点data失败:', e);
- }
- }
- const config = nodeConfigs.find(c => c.type === (nodeDO.type || nodeData.type));
- setWorkflowNodeConfig({
- nodeName: nodeDO.nodeName || nodeData.label || config?.label || '',
- nodeIcon: nodeDO.nodeIcon || nodeData.icon || '',
- responsible: (nodeDO.workerUserId !== null && nodeDO.workerUserId !== undefined && nodeDO.workerUserId !== 0)
- ? (typeof nodeDO.workerUserId === 'number' ? nodeDO.workerUserId : Number(nodeDO.workerUserId))
- : (nodeData.workerUserId && nodeData.workerUserId !== '' && nodeData.workerUserId !== '0')
- ? (typeof nodeData.workerUserId === 'number' ? nodeData.workerUserId : Number(nodeData.workerUserId))
- : undefined,
- remark: nodeData.remark || '',
- submitForm: nodeDO.formId ? (typeof nodeDO.formId === 'number' ? nodeDO.formId : Number(nodeDO.formId)) : (nodeData.submitForm ? (typeof nodeData.submitForm === 'number' ? nodeData.submitForm : Number(nodeData.submitForm)) : undefined),
- isolationType: (nodeDO.isolationType !== null && nodeDO.isolationType !== undefined) ? String(nodeDO.isolationType) : (nodeData.isolationType || ''),
- isolationPoints: nodeDO.isolationPoints ? (typeof nodeDO.isolationPoints === 'string' ? JSON.parse(nodeDO.isolationPoints) : nodeDO.isolationPoints) : (nodeData.isolationPoints || []),
- isolationNode: nodeData.isolationNode || [],
- isolationNodeUuid: nodeDO.isolationNodeUuid || nodeData.isolationNodeUuid || '',
- lockPerson: (() => {
- let lockPersonId: number | undefined = undefined;
- if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
- const lockerUser = nodeDO.nodeUserList.find((user: any) => user.type === 'jtlocker');
- if (lockerUser?.userId) {
- lockPersonId = typeof lockerUser.userId === 'number' ? lockerUser.userId : Number(lockerUser.userId);
- }
- }
- return lockPersonId || (nodeData.lockPerson ? (typeof nodeData.lockPerson === 'number' ? nodeData.lockPerson : Number(nodeData.lockPerson)) : undefined);
- })(),
- coLockPersons: (() => {
- const coLockPersonIds: number[] = [];
- if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
- nodeDO.nodeUserList.forEach((user: any) => {
- if (user.type === 'jtcolocker' && user.userId) {
- coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
- }
- });
- }
- return coLockPersonIds.length > 0 ? coLockPersonIds : (nodeData.coLockPersons && Array.isArray(nodeData.coLockPersons) ? nodeData.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
- })(),
- notificationMethods: nodeData.notificationMethods || {
- sms: false,
- message: false,
- email: false,
- app: false,
- },
- notificationPerson: nodeData.notificationPerson || '',
- notificationTime: nodeDO.notifyTime || nodeData.notificationTime || '',
- });
- } else {
- // 如果既没有缓存也没有 nodeDO,使用节点原始数据
- const source = firstUnsavedNode.data || {};
- const config = nodeConfigs.find(c => c.type === source.type);
- setWorkflowNodeConfig({
- nodeName: source.label || config?.label || '',
- nodeIcon: source.icon || '',
- responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
- remark: source.remark || '',
- submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
- isolationType: source.isolationType || '',
- isolationPoints: source.isolationPoints || [],
- isolationNode: source.isolationNode || [],
- isolationNodeUuid: source.isolationNodeUuid || '',
- lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
- coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
- notificationMethods: source.notificationMethods || {
- sms: false,
- message: false,
- email: false,
- app: false,
- },
- notificationPerson: source.notificationPerson || '',
- notificationTime: source.notificationTime || '',
- });
- }
- }
-
- console.log('自动选中第一个未保存的节点:', firstUnsavedNode.id);
-
- // 高亮选中的节点
- return currentNodes.map((node) =>
- node.id === firstUnsavedNode.id
- ? { ...node, selected: true }
- : { ...node, selected: false }
- );
- });
- }, 300); // 延迟300ms,确保 workflowNodes 已经渲染完成
- }
- } catch (error: any) {
- console.error('初始化节点状态失败:', error);
- message.error('加载节点状态失败: ' + (error?.message || '未知错误'));
- }
- };
-
- loadRoleUsers();
- loadIsolationPoints();
- loadFormList();
- initializeNodeStates();
- }
- }, [workJobStep, workflowWorkId]);
-
- // 验证节点配置是否完整
- const validateNodeConfig = useCallback((config: any, nodeType: string): boolean => {
- // 节点名称必填
- if (!config.nodeName || config.nodeName.trim() === '') {
- return false;
- }
-
- // 业务表单必填(只有确认节点、审核节点、录入信息节点需要必填)
- const formRequiredNodeTypes = ['confirm', 'review', 'inputInfo'];
- if (formRequiredNodeTypes.includes(nodeType) && !config.submitForm) {
- return false;
- }
-
- // 负责人必填(创建作业、隔离、解除隔离节点除外)
- // 对于隔离节点,负责人验证在隔离方式验证中处理
- if (nodeType !== 'createJob' && nodeType !== 'isolation' && nodeType !== 'releaseIsolation') {
- if (!config.responsible) {
- return false;
- }
- }
-
- // 隔离/方案节点特殊验证
- if (nodeType === 'isolation') {
- if (!config.isolationType || config.isolationType === '') {
- return false;
- }
- if (config.isolationPoints.length === 0) {
- return false;
- }
- // 字典值:0=盲板,1=上锁挂牌,2=拆除
- if (config.isolationType === '0' || config.isolationType === '2') {
- if (!config.responsible) {
- return false;
- }
- }
- if (config.isolationType === '1') {
- if (!config.lockPerson) {
- return false;
- }
- }
- }
-
- // 解除隔离节点特殊验证
- if (nodeType === 'releaseIsolation') {
- if (!config.isolationNodeUuid || config.isolationNodeUuid === '') {
- return false;
- }
- }
-
- return true;
- }, []);
-
- // 获取下一个未完成的节点(基于 workflowJson 中的 adjacency 信息)
- // 规则:
- // 1. 从开始节点(parentUuid 为空的节点)开始
- // 2. 按照 adjacency 中的 childrenUuid 顺序执行
- // 3. 如果有多个子节点,按顺序一个一个执行
- // 4. 所有子节点完成后,再找下一个节点
- const getNextIncompleteNode = useCallback((completedSet?: Set<string>, currentNodeId?: string): Node | null => {
- const completed = completedSet || completedNodeIds;
-
- // 如果所有节点都已完成,返回null
- if (completed.size === workflowNodes.length) {
- return null;
- }
-
- // 从 workflowJson 中获取 adjacency 信息
- if (!workflowJson || !workflowJson.adjacency) {
- // 如果没有 adjacency 信息,回退到使用边的逻辑
- console.warn('workflowJson 中没有 adjacency 信息,使用边的逻辑');
- return null;
- }
-
- const adjacency = workflowJson.adjacency;
- const nodeMap = new Map<string, Node>(); // 节点ID -> 节点对象
-
- // 初始化节点映射
- workflowNodes.forEach(node => {
- nodeMap.set(node.id, node);
- });
-
- // 辅助函数:检查节点的所有父节点是否都已完成
- const areAllParentsCompleted = (nodeId: string): boolean => {
- const nodeInfo = adjacency[nodeId];
- if (!nodeInfo || !nodeInfo.parentUuid || nodeInfo.parentUuid.length === 0) {
- return true; // 没有父节点,可以执行
- }
- return nodeInfo.parentUuid.every((parentId: string) => completed.has(parentId));
- };
-
- // 辅助函数:获取节点的第一个未完成的子节点
- const getFirstIncompleteChild = (nodeId: string): string | null => {
- const nodeInfo = adjacency[nodeId];
- if (!nodeInfo || !nodeInfo.childrenUuid || nodeInfo.childrenUuid.length === 0) {
- return null; // 没有子节点
- }
-
- // 按顺序查找第一个未完成的子节点
- for (const childId of nodeInfo.childrenUuid) {
- if (!completed.has(childId) && areAllParentsCompleted(childId)) {
- return childId;
- }
- }
-
- return null;
- };
-
- // 如果提供了当前节点ID,优先查找当前节点的下一个子节点
- if (currentNodeId && adjacency[currentNodeId]) {
- const nextChildId = getFirstIncompleteChild(currentNodeId);
- if (nextChildId) {
- const nextNode = nodeMap.get(nextChildId);
- if (nextNode) {
- return nextNode;
- }
- }
- }
-
- // 如果没有当前节点或当前节点的子节点都已完成,使用BFS查找下一个可执行的节点
- const queue: string[] = [];
- const visited = new Set<string>();
-
- // 找到所有开始节点(parentUuid 为空的节点)
- for (const nodeId in adjacency) {
- const nodeInfo = adjacency[nodeId];
- if (!nodeInfo.parentUuid || nodeInfo.parentUuid.length === 0) {
- if (!completed.has(nodeId)) {
- // 如果开始节点未完成,直接返回
- const node = nodeMap.get(nodeId);
- if (node) {
- return node;
- }
- } else {
- // 如果开始节点已完成,将其子节点加入队列
- if (nodeInfo.childrenUuid && nodeInfo.childrenUuid.length > 0) {
- nodeInfo.childrenUuid.forEach((childId: string) => {
- if (!visited.has(childId)) {
- queue.push(childId);
- visited.add(childId);
- }
- });
- }
- }
- }
- }
-
- // BFS遍历查找下一个可执行的未完成节点
- while (queue.length > 0) {
- const currentNodeId = queue.shift()!;
-
- // 如果当前节点未完成,检查是否可执行(所有父节点都已完成)
- if (!completed.has(currentNodeId) && areAllParentsCompleted(currentNodeId)) {
- const node = nodeMap.get(currentNodeId);
- if (node) {
- return node;
- }
- }
-
- // 如果当前节点已完成,将其子节点加入队列(按顺序)
- if (completed.has(currentNodeId)) {
- const nodeInfo = adjacency[currentNodeId];
- if (nodeInfo && nodeInfo.childrenUuid && nodeInfo.childrenUuid.length > 0) {
- nodeInfo.childrenUuid.forEach((childId: string) => {
- if (!visited.has(childId)) {
- queue.push(childId);
- visited.add(childId);
- }
- });
- }
- }
- }
-
- return null;
- }, [workflowNodes, workflowEdges, completedNodeIds, workflowJson]);
-
- // 节点点击事件
- const onWorkflowNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
- console.log('点击节点:', node.id);
-
- // 更新节点选中状态
- setWorkflowNodes((nds) =>
- nds.map((n) =>
- n.id === node.id
- ? { ...n, selected: true }
- : { ...n, selected: false }
- )
- );
-
- // 先设置选中的节点,确保右侧面板显示
- setSelectedWorkflowNode(node);
-
- // 优先从 workflowWorkNodeDOList 中查找对应节点数据(通过uuid匹配)
- const nodeDO = workflowWorkNodeDOList.find(item => item.uuid === node.id);
-
- console.log('点击节点:', node.id, '找到的 nodeDO:', nodeDO);
- console.log('当前 workflowWorkNodeDOList:', workflowWorkNodeDOList);
-
- if (nodeDO) {
- // 如果找到节点数据,解析data字段并回显
- let nodeData: any = {};
- if (nodeDO.data) {
- try {
- nodeData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
- } catch (e) {
- console.error('解析节点data失败:', e);
- }
- }
-
- // 从 nodeUserList 中提取上锁人和共锁人
- let lockPersonId: string | number = '';
- const coLockPersonIds: (string | number)[] = [];
- if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
- nodeDO.nodeUserList.forEach((user: any) => {
- if (user.type === 'jtlocker' && user.userId) {
- lockPersonId = typeof user.userId === 'number' ? user.userId : Number(user.userId);
- } else if (user.type === 'jtcolocker' && user.userId) {
- coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
- }
- });
- }
-
- // 优先使用 nodeDO 中的数据(编辑时从后端获取的真实数据)
- // 如果 nodeDO 存在且有 id,说明是已保存的节点,优先使用 nodeDO 的数据
- const source = nodeData || node.data || {};
- const config = nodeConfigs.find(c => c.type === (nodeDO.type || source.type));
-
- // 解析隔离点
- let isolationPoints: string[] = [];
- if (nodeDO.isolationPoints) {
- try {
- isolationPoints = typeof nodeDO.isolationPoints === 'string'
- ? JSON.parse(nodeDO.isolationPoints)
- : (Array.isArray(nodeDO.isolationPoints) ? nodeDO.isolationPoints : []);
- } catch (e) {
- console.error('解析隔离点失败:', e);
- isolationPoints = source.isolationPoints || [];
- }
- } else {
- isolationPoints = source.isolationPoints || [];
- }
-
- // 构建节点配置,优先使用 nodeDO 中的数据
- console.log('构建节点配置 - nodeDO.workerUserId:', nodeDO.workerUserId, 'source.workerUserId:', source.workerUserId);
- const nodeConfig = {
- nodeName: nodeDO.nodeName || source.label || config?.label || '',
- nodeIcon: nodeDO.nodeIcon || source.icon || '',
- responsible: (nodeDO.workerUserId !== null && nodeDO.workerUserId !== undefined && nodeDO.workerUserId !== 0)
- ? (typeof nodeDO.workerUserId === 'number' ? nodeDO.workerUserId : Number(nodeDO.workerUserId))
- : (source.workerUserId && source.workerUserId !== '' && source.workerUserId !== '0')
- ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId))
- : undefined,
- remark: source.remark || '',
- submitForm: nodeDO.formId ? (typeof nodeDO.formId === 'number' ? nodeDO.formId : Number(nodeDO.formId)) : (source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined),
- isolationType: (nodeDO.isolationType !== null && nodeDO.isolationType !== undefined) ? String(nodeDO.isolationType) : (source.isolationType || ''),
- isolationPoints: isolationPoints,
- isolationNode: source.isolationNode || [],
- isolationNodeUuid: nodeDO.isolationNodeUuid || source.isolationNodeUuid || '',
- lockPerson: lockPersonId || (source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined),
- coLockPersons: coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []),
- notificationMethods: source.notificationMethods || {
- sms: false,
- message: false,
- email: false,
- app: false,
- },
- notificationPerson: source.notificationPerson || '',
- notificationTime: nodeDO.notifyTime || source.notificationTime || '',
- smsTemplateCode: source.smsTemplateCode || 'false',
- messageTemplateCode: source.messageTemplateCode || 'false',
- emailTemplateCode: source.emailTemplateCode || 'false',
- appTemplateCode: source.appTemplateCode || 'false',
- };
-
- // 优先使用缓存中的配置(如果有),这样可以保持用户未保存的修改
- // 如果缓存中没有,再使用 nodeDO 的数据
- console.log('设置节点配置 - nodeConfig.responsible:', nodeConfig.responsible, 'node.id:', node.id);
- const cachedConfig = workflowNodeConfigCache.get(node.id);
- console.log('缓存中的配置:', cachedConfig);
- if (cachedConfig) {
- // 如果缓存中有配置,使用缓存(可能包含用户未保存的修改)
- console.log('使用缓存配置');
- // 使用函数式更新确保状态正确更新
- setWorkflowNodeConfig(() => ({ ...cachedConfig }));
- } else if (nodeDO.id) {
- // 如果缓存中没有,但节点已保存,使用 nodeDO 的数据
- console.log('使用 nodeDO 配置');
- setWorkflowNodeConfig(() => ({ ...nodeConfig }));
- } else {
- // 如果既没有缓存也没有保存,使用节点数据
- console.log('使用节点数据配置');
- setWorkflowNodeConfig(() => ({ ...nodeConfig }));
- }
- } else {
- // 如果没有找到节点数据,使用原有逻辑
- console.log('没有找到 nodeDO,使用节点数据');
- const cachedConfig = workflowNodeConfigCache.get(node.id);
- if (cachedConfig) {
- console.log('使用缓存配置(无 nodeDO)');
- setWorkflowNodeConfig(() => cachedConfig);
- } else {
- console.log('使用节点原始数据');
- const source = node.data || {};
- const config = nodeConfigs.find(c => c.type === source.type);
- setWorkflowNodeConfig({
- nodeName: source.label || config?.label || '',
- nodeIcon: source.icon || '',
- responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
- remark: source.remark || '',
- submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
- isolationType: source.isolationType || '',
- isolationPoints: source.isolationPoints || [],
- isolationNode: source.isolationNode || [],
- isolationNodeUuid: source.isolationNodeUuid || '',
- lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
- coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
- 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',
- });
- }
- }
- setWorkflowActiveTabKey('info');
- }, [workflowNodeConfigCache, workflowWorkNodeDOList, nodeConfigs]);
-
- // 画布点击事件(取消选择)
- const onWorkflowPaneClick = useCallback(() => {
- setSelectedWorkflowNode(null);
- }, []);
-
- // 实时更新节点显示(只在 workflowNodeConfig 变化时更新,避免在切换节点时覆盖)
- useEffect(() => {
- if (selectedWorkflowNode && workflowNodeConfig.nodeName) {
- const { isolationMethod, ...restData } = selectedWorkflowNode.data || {};
- // 从缓存中读取 completed 状态,保持保存状态不变
- const isSaved = nodeSavedStatusCache.get(selectedWorkflowNode.id) || false;
- const updatedData = {
- ...restData,
- label: workflowNodeConfig.nodeName,
- icon: workflowNodeConfig.nodeIcon,
- responsible: workflowNodeConfig.responsible,
- remark: workflowNodeConfig.remark,
- submitForm: workflowNodeConfig.submitForm,
- isolationType: workflowNodeConfig.isolationType,
- isolationPoints: workflowNodeConfig.isolationPoints,
- isolationNode: workflowNodeConfig.isolationNode,
- isolationNodeUuid: workflowNodeConfig.isolationNodeUuid,
- lockPerson: workflowNodeConfig.lockPerson,
- coLockPersons: workflowNodeConfig.coLockPersons,
- notificationMethods: workflowNodeConfig.notificationMethods,
- notificationPerson: workflowNodeConfig.notificationPerson,
- notificationTime: workflowNodeConfig.notificationTime,
- smsTemplateCode: workflowNodeConfig.smsTemplateCode,
- messageTemplateCode: workflowNodeConfig.messageTemplateCode,
- emailTemplateCode: workflowNodeConfig.emailTemplateCode,
- appTemplateCode: workflowNodeConfig.appTemplateCode,
- // 保持 completed 状态从缓存中读取,不覆盖
- completed: isSaved,
- };
- setWorkflowNodes((nds) =>
- nds.map((node) =>
- node.id === selectedWorkflowNode.id
- ? { ...node, data: updatedData }
- : node
- )
- );
- }
- }, [workflowNodeConfig, selectedWorkflowNode?.id, setWorkflowNodes, nodeSavedStatusCache]);
- // 作业管理删除处理
- const handleWorkJobDelete = async (id: number) => {
- Modal.confirm({
- title: t('common.confirmDeleteWork'),
- content: t('common.confirmDeleteWorkText'),
- okText: t('common.confirmDeleteWorkButton'),
- okType: 'danger',
- cancelText: t('common.cancel'),
- onOk: async () => {
- try {
- await workJobApi.deleteWorkflowWorkList([id]);
- message.success(t('common.deleteWorkSuccess'));
- // 删除成功后刷新列表
- if (workJobList.length === 1 && workJobPagination.pageNo > 1) {
- setWorkJobPagination({ ...workJobPagination, pageNo: workJobPagination.pageNo - 1 });
- } else {
- await getWorkJobList();
- }
- } catch (error: any) {
- console.error('删除失败:', error);
- message.error(error?.message || t('common.deleteWorkFailed'));
- }
- },
- });
- };
- // 作业管理编辑处理
- const handleWorkJobEdit = async (item: WorkJobVO, viewMode: boolean = false) => {
- // 如果明确指定了 viewMode,设置查看模式
- if (viewMode) {
- setIsViewMode(true);
- } else {
- // 否则,如果是直接调用(不是通过 handleWorkJobView),确保是编辑模式
- setIsViewMode(false);
- }
- try {
- // 调用详情接口获取完整数据
- const response = await workJobApi.selectWorkflowWorkById(item.id!);
- // axios拦截器可能已经处理了数据格式,直接使用response
- const detail = response as any;
-
- // 设置作业ID,这样点击"下一步"时会调用更新接口
- setWorkflowWorkId(detail.id);
-
- // 重置表单和步骤
- setWorkJobStep(0);
- workJobBasicForm.resetFields();
- workJobPublishForm.resetFields();
-
- // 加载字典数据和流程模板列表
- await Promise.all([
- getWorkTypeDictList(),
- getUrgencyLevelDictList(),
- getIsolationMethodDictList(),
- getWorkflowTemplateList(),
- ]);
-
- // 回显基本信息表单数据
- const formData = {
- workflowTemplate: detail.designId || undefined,
- jobCategory: detail.type || undefined,
- jobName: detail.name || '',
- jobContent: detail.description || '',
- urgency: detail.urgencyLevel || undefined,
- };
- workJobBasicForm.setFieldsValue(formData);
-
- // 保存原始表单数据(用于判断是否有修改)
- setOriginalBasicFormData(formData);
-
- // 保存 workflowWorkNodeDOList
- if (detail.workflowWorkNodeDOList && Array.isArray(detail.workflowWorkNodeDOList)) {
- setWorkflowWorkNodeDOList(detail.workflowWorkNodeDOList);
- } else {
- setWorkflowWorkNodeDOList([]);
- }
-
- // 如果有流程设计内容,加载流程设计JSON
- // 优先使用 designContent(如果存在),否则使用 designId 调用接口
- // 但如果 designContent 和 workflowWorkNodeDOList 都为 null,应该清空流程
- const hasDesignContent = detail.designContent && detail.designContent !== null;
- const hasWorkflowNodes = detail.workflowWorkNodeDOList && Array.isArray(detail.workflowWorkNodeDOList) && detail.workflowWorkNodeDOList.length > 0;
-
- if (hasDesignContent) {
- try {
- // 如果详情中直接包含流程设计内容,直接解析并设置
- const jsonData = typeof detail.designContent === 'string'
- ? JSON.parse(detail.designContent)
- : detail.designContent;
- setWorkflowJson(jsonData);
-
- // 调用 loadWorkflowJson 的逻辑来渲染节点和连线
- // 这里直接使用已有的 loadWorkflowJson 函数,但需要先设置 workflowJson
- // 然后手动触发渲染逻辑
- if (jsonData && jsonData.nodes && Array.isArray(jsonData.nodes) && jsonData.edges && Array.isArray(jsonData.edges)) {
- // 使用 loadWorkflowJson 中的渲染逻辑
- // 先创建节点映射,方便后续查找
- const nodeDOMap = new Map<string, WorkflowWorkNodeDO>();
- detail.workflowWorkNodeDOList?.forEach((nodeDO: WorkflowWorkNodeDO) => {
- if (nodeDO.uuid) {
- nodeDOMap.set(nodeDO.uuid, nodeDO);
- }
- });
-
- const importedNodes: Node[] = jsonData.nodes.map((node: any) => {
- const nodeData = node.data || {};
- const nodeId = node.id || node.uuid;
- const nodeDO = nodeDOMap.get(nodeId);
-
- // 优先使用 nodeDO 中的 position,否则使用 JSON 中的 position
- let nodePosition = { x: 0, y: 0 };
- if (nodeDO?.position) {
- try {
- nodePosition = typeof nodeDO.position === 'string' ? JSON.parse(nodeDO.position) : nodeDO.position;
- } catch (e) {
- console.error('解析节点position失败:', e);
- nodePosition = node.position || { x: 0, y: 0 };
- }
- } else {
- nodePosition = node.position || { x: 0, y: 0 };
- }
-
- // 如果 nodeDO 中有 data,优先使用 nodeDO 中的数据
- let finalNodeData = nodeData;
- if (nodeDO?.data) {
- try {
- const nodeDOData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
- finalNodeData = { ...nodeData, ...nodeDOData };
- } catch (e) {
- console.error('解析节点data失败:', e);
- }
- }
-
- return {
- id: nodeId,
- type: node.type || nodeDO?.type || 'createJob',
- position: nodePosition,
- data: (() => {
- const { isolationMethod, ...restFinalNodeData } = finalNodeData || {};
- return {
- ...restFinalNodeData,
- label: nodeDO?.nodeName || finalNodeData.label || node.label || nodeConfigs.find(c => c.type === (node.type || finalNodeData.type))?.label || '节点',
- type: node.type || nodeDO?.type || finalNodeData.type || 'createJob',
- // 标记节点是否已完成配置(如果 nodeDO 存在且有 id,说明已经保存过)
- completed: !!nodeDO?.id,
- };
- })(),
- };
- });
-
- const importedEdges: Edge[] = jsonData.edges.map((edge: any) => {
- let sourceHandle = edge.sourceHandle;
- let targetHandle = edge.targetHandle;
-
- if (!sourceHandle && edge.id) {
- const sourceMatch = edge.id.match(/right-source|left-source|top-source|bottom-source/);
- if (sourceMatch) {
- sourceHandle = sourceMatch[0].replace('-source', '');
- } else {
- sourceHandle = 'right-source';
- }
- } else if (!sourceHandle) {
- sourceHandle = 'right-source';
- }
-
- if (!targetHandle && edge.id) {
- const targetMatch = edge.id.match(/right-target|left-target|top-target|bottom-target/);
- if (targetMatch) {
- targetHandle = targetMatch[0].replace('-target', '');
- } else {
- targetHandle = 'left-target';
- }
- } else if (!targetHandle) {
- targetHandle = 'left-target';
- }
-
- return {
- id: edge.id || `${edge.source}-${edge.target}`,
- source: edge.source,
- target: edge.target,
- sourceHandle: sourceHandle,
- targetHandle: targetHandle,
- type: 'straight',
- style: { strokeWidth: 2, stroke: '#000000' },
- markerStart: {
- type: 'arrowclosed',
- color: '#000000',
- },
- };
- });
-
- setWorkflowNodes(importedNodes);
- setWorkflowEdges(importedEdges);
-
- // 清空之前的缓存和完成状态
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- // 清除节点保存状态缓存
- setNodeSavedStatusCache(new Map());
-
- // 自动选中第一个节点
- if (importedNodes.length > 0) {
- setTimeout(() => {
- const firstNode = importedNodes[0];
- setSelectedWorkflowNode(firstNode);
-
- // 查找对应的 nodeDO
- const firstNodeDO = nodeDOMap.get(firstNode.id);
-
- if (firstNodeDO) {
- // 如果找到 nodeDO,从 nodeDO 中读取数据
- let nodeData: any = {};
- if (firstNodeDO.data) {
- try {
- nodeData = typeof firstNodeDO.data === 'string' ? JSON.parse(firstNodeDO.data) : firstNodeDO.data;
- } catch (e) {
- console.error('解析节点data失败:', e);
- }
- }
-
- // 从 nodeUserList 中提取上锁人和共锁人
- let lockPersonId: string | number = '';
- const coLockPersonIds: (string | number)[] = [];
- if (firstNodeDO.nodeUserList && Array.isArray(firstNodeDO.nodeUserList)) {
- firstNodeDO.nodeUserList.forEach((user: any) => {
- if (user.type === 'jtlocker' && user.userId) {
- lockPersonId = typeof user.userId === 'number' ? user.userId : Number(user.userId);
- } else if (user.type === 'jtcolocker' && user.userId) {
- coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
- }
- });
- }
-
- const source = nodeData || firstNode.data || {};
- const config = nodeConfigs.find(c => c.type === (firstNodeDO.type || source.type));
-
- // 解析隔离点
- let isolationPoints: string[] = [];
- if (firstNodeDO.isolationPoints) {
- try {
- isolationPoints = typeof firstNodeDO.isolationPoints === 'string'
- ? JSON.parse(firstNodeDO.isolationPoints)
- : (Array.isArray(firstNodeDO.isolationPoints) ? firstNodeDO.isolationPoints : []);
- } catch (e) {
- console.error('解析隔离点失败:', e);
- isolationPoints = source.isolationPoints || [];
- }
- } else {
- isolationPoints = source.isolationPoints || [];
- }
-
- setWorkflowNodeConfig({
- nodeName: firstNodeDO.nodeName || source.label || config?.label || '',
- nodeIcon: firstNodeDO.nodeIcon || source.icon || '',
- responsible: firstNodeDO.workerUserId ? (typeof firstNodeDO.workerUserId === 'number' ? firstNodeDO.workerUserId : Number(firstNodeDO.workerUserId)) : (source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined),
- remark: source.remark || '',
- submitForm: firstNodeDO.formId ? (typeof firstNodeDO.formId === 'number' ? firstNodeDO.formId : Number(firstNodeDO.formId)) : (source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined),
- isolationType: (firstNodeDO.isolationType !== null && firstNodeDO.isolationType !== undefined) ? String(firstNodeDO.isolationType) : (source.isolationType || ''),
- isolationPoints: isolationPoints,
- isolationNode: source.isolationNode || [],
- isolationNodeUuid: firstNodeDO.isolationNodeUuid || source.isolationNodeUuid || '',
- lockPerson: lockPersonId || source.lockPerson || '',
- coLockPersons: coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons || []),
- notificationMethods: source.notificationMethods || {
- sms: false,
- message: false,
- email: false,
- app: false,
- },
- notificationPerson: source.notificationPerson || '',
- notificationTime: firstNodeDO.notifyTime || source.notificationTime || '',
- });
- } else {
- // 如果没有找到 nodeDO,使用原有逻辑
- const source = firstNode.data || {};
- const config = nodeConfigs.find(c => c.type === source.type);
- setWorkflowNodeConfig({
- nodeName: source.label || config?.label || '',
- nodeIcon: source.icon || '',
- responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
- remark: source.remark || '',
- submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
- isolationType: source.isolationType || '',
- isolationPoints: source.isolationPoints || [],
- 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 || '',
- });
- }
-
- setWorkflowNodes(prev => prev.map(n => ({
- ...n,
- selected: n.id === firstNode.id,
- })));
- }, 100);
- }
- }
- } catch (error: any) {
- console.error('解析流程设计内容失败:', error);
- // 如果解析失败,清空流程
- setWorkflowJson(null);
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- setNodeSavedStatusCache(new Map());
- }
- } else if (!hasDesignContent && !hasWorkflowNodes) {
- // 如果 designContent 和 workflowWorkNodeDOList 都为 null,清空流程
- console.log('designContent 和 workflowWorkNodeDOList 都为 null,清空流程');
- setWorkflowJson(null);
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- setNodeSavedStatusCache(new Map());
- setSelectedWorkflowNode(null);
- } else if (detail.designId && hasWorkflowNodes) {
- // 只有在有 workflowWorkNodeDOList 数据时,才尝试通过 designId 加载模板
- // 这样可以避免在编辑时,即使没有流程数据也加载模板
- try {
- await loadWorkflowJson(detail.designId);
- } catch (error: any) {
- console.error('加载流程设计失败:', error);
- // 加载失败时也清空流程
- setWorkflowJson(null);
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- setNodeSavedStatusCache(new Map());
- }
- }
-
- // 打开多步骤弹框
- setShowAddModal(true);
- setEditingItem(null); // 确保不是编辑模式(使用新增的多步骤弹框)
- } catch (error: any) {
- console.error('获取作业详情失败:', error);
- message.error(error?.message || '获取作业详情失败');
- }
- };
- // 作业管理查看处理(跳转到作业详情页面)
- const handleWorkJobView = (item: WorkJobVO) => {
- // 跳转到作业详情页面
- const workId = item.id;
- if (workId) {
- navigate(`/work-job/detail?id=${workId}`);
- } else {
- message.warning(t('common.workIdNotExistsForView'));
- }
- };
-
- // 为查看模式加载流程设计JSON
- const loadWorkflowJsonForView = async (workflowId: number) => {
- try {
- const response = await workflowDesignApi.selectWorkflowDesignById(workflowId);
- const workflow = response as any;
- // 检查 content 是否为 null 或空字符串
- const hasContent = workflow.content && workflow.content !== null && workflow.content !== '';
-
- if (hasContent) {
- try {
- const jsonData = typeof workflow.content === 'string'
- ? JSON.parse(workflow.content)
- : workflow.content;
- setViewWorkflowJson(jsonData);
- } catch (parseError) {
- console.error('解析流程设计JSON失败:', parseError);
- // 解析失败时清空
- setViewWorkflowJson(null);
- }
- } else {
- // content 为 null 或空时,清空流程
- console.warn('流程设计content为空或null,清空查看模式的流程');
- setViewWorkflowJson(null);
- }
- } catch (error: any) {
- console.error('加载流程设计失败:', error);
- // 加载失败时也清空
- setViewWorkflowJson(null);
- }
- };
- // 根据当前子菜单获取数据和列配置
- const getTableConfig = () => {
- if (subMenu === '表单管理') {
- // 表单管理使用单独的过滤逻辑
- let filtered = formManagementData;
- if (formManagementQuery.name) {
- filtered = filtered.filter(item =>
- item.name.toLowerCase().includes(formManagementQuery.name.toLowerCase())
- );
- }
- if (formManagementQuery.status) {
- filtered = filtered.filter(item => item.status === formManagementQuery.status);
- }
- return {
- data: filtered,
- columns: [
- { key: 'name', label: t('table.formName'), width: '20%' },
- { key: 'status', label: t('table.status'), width: '10%' },
- { key: 'description', label: t('table.remark'), width: '25%' },
- { key: 'createTime', label: t('table.createTime'), width: '15%' },
- ],
- };
- } else if (subMenu === '流程设计') {
- // 流程设计使用单独的过滤逻辑
- let filtered = processDesignData;
- if (processDesignQuery.name) {
- filtered = filtered.filter(item =>
- item.name.toLowerCase().includes(processDesignQuery.name.toLowerCase())
- );
- }
- return {
- data: filtered,
- columns: [
- { key: 'name', label: t('table.name') || '名称', width: '20%' },
- { key: 'designer', label: t('table.designer'), width: '10%' },
- { key: 'designTime', label: t('table.designTime'), width: '15%' },
- { key: 'nodeCount', label: t('table.nodeCount'), width: '10%' },
- { key: 'description', label: t('table.description'), width: '25%' },
- { key: 'status', label: t('table.status'), width: '10%' },
- ],
- };
- } else if (subMenu === '流程模板') {
- return {
- data: templateData,
- columns: [
- { key: 'code', label: t('table.templateCode'), width: '10%' },
- { key: 'name', label: t('table.templateName'), width: '15%' },
- { key: 'type', label: t('table.templateType'), width: '10%' },
- { key: 'category', label: t('table.templateCategory'), width: '10%' },
- { key: 'steps', label: t('table.stepCount'), width: '8%' },
- { key: 'approvalLevel', label: t('table.approvalLevel'), width: '10%' },
- { key: 'creator', label: t('table.creator'), width: '8%' },
- { key: 'status', label: t('table.status'), width: '8%' },
- { key: 'useCount', label: t('table.useCount'), width: '9%' },
- { key: 'createTime', label: t('table.createTime'), width: '12%' },
- ],
- };
- } else if (subMenu === 'SOP管理') {
- return {
- data: sopData,
- columns: [
- { key: 'code', label: 'SOP编号', width: '10%' },
- { key: 'name', label: 'SOP名称', width: '15%' },
- { key: 'version', label: '版本', width: '8%' },
- { key: 'category', label: '分类', width: '10%' },
- { key: 'department', label: '所属部门', width: '10%' },
- { key: 'effectiveDate', label: '生效日期', width: '10%' },
- { key: 'reviewDate', label: '复审日期', width: '10%' },
- { key: 'status', label: '状态', width: '8%' },
- { key: 'reviewer', label: '审核人', width: '8%' },
- { key: 'attachments', label: '附件数', width: '8%' },
- ],
- };
- } else if (subMenu === '作业管理') {
- // 作业管理使用API数据,不需要在这里返回
- return {
- data: [],
- columns: [],
- };
- }
- return { data: [], columns: [] };
- };
- const { data, columns } = getTableConfig();
- // 过滤数据(表单管理和流程设计除外,因为它们有自己的搜索逻辑)
- const filteredData = (subMenu === '表单管理' || subMenu === '流程设计')
- ? data
- : data.filter((item) =>
- Object.values(item).some((value) =>
- String(value).toLowerCase().includes(searchTerm.toLowerCase())
- )
- );
- const handleDelete = async (id: number) => {
- const isProcessTemplate = subMenu === '流程模板';
- Modal.confirm({
- title: t('common.confirmDelete'),
- content: isProcessTemplate ? t('common.deleteProcessTemplate') : `${t('common.confirmDeleteText')} ${t('isolationWork.processDesign')}?${t('common.confirmDeleteWarning')}`,
- okText: t('common.confirmDeleteButton'),
- okType: 'danger',
- cancelText: t('common.cancel'),
- onOk: async () => {
- try {
- await workflowDesignApi.deleteWorkflowDesignList([id]);
- message.success(t('common.deleteSuccess'));
- // 删除成功后刷新列表
- // 如果当前页只有一条数据且不是第一页,删除后跳转到上一页
- if (processDesignList.length === 1 && processDesignPagination.pageNo > 1) {
- setProcessDesignPagination({ ...processDesignPagination, pageNo: processDesignPagination.pageNo - 1 });
- } else {
- // 否则直接刷新当前页数据
- await getProcessDesignList();
- }
- } catch (error: any) {
- console.error('删除失败:', error);
- message.error(error?.message || t('common.deleteFailed'));
- }
- },
- });
- };
- const handleEdit = (item: TableRow | WorkflowDesignVO) => {
- setEditingItem(item as TableRow);
- // 设置表单初始值
- form.setFieldsValue({
- name: item.name || '',
- description: item.description || '',
- });
- setShowAddModal(true);
- };
- const handleDesign = (item: TableRow | WorkflowDesignVO) => {
- console.log('设计流程:', item);
- // 跳转到流程设计页面,传递流程ID
- if (item.id) {
- // 记录来源菜单:用于从流程设计器返回时恢复菜单到「隔离作业 > 流程模板」
- sessionStorage.setItem(
- 'processDesignerSource',
- JSON.stringify({ menu: 'isolationWork', subMenu: 'processTemplate' })
- );
- navigate(`/process-designer?id=${item.id}`);
- }
- };
- // 获取表单字段
- const getFormFields = () => {
- if (subMenu === '流程模板') {
- return (
- <div className="grid grid-cols-2 gap-4">
- <div>
- <label className="block text-sm text-gray-700 mb-2">{t('common.templateCode')} *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder={t('common.templateCodePlaceholder')}
- defaultValue={editingItem?.code || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">{t('form.templateName')} *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder={t('form.templateNamePlaceholder')}
- defaultValue={editingItem?.name || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">{t('common.processType')} *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">{t('common.processTypePlaceholder')}</option>
- <option value="标准流程">{t('common.standardProcess')}</option>
- <option value="应急流程">{t('common.emergencyProcess')}</option>
- <option value="临时流程">{t('common.temporaryProcess')}</option>
- </select>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">{t('common.workCategory')} *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">{t('common.workCategoryPlaceholder')}</option>
- <option value="高压作业">{t('common.highVoltageWork')}</option>
- <option value="低压作业">{t('common.lowVoltageWork')}</option>
- <option value="应急处理">{t('common.emergencyHandling')}</option>
- <option value="定期维护">{t('common.regularMaintenance')}</option>
- </select>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">{t('common.approvalLevel')} *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">{t('common.approvalLevelPlaceholder')}</option>
- <option value="一级审批">{t('common.firstLevelApproval')}</option>
- <option value="二级审批">{t('common.secondLevelApproval')}</option>
- <option value="特殊审批">{t('common.specialApproval')}</option>
- </select>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">{t('common.stepCount')}</label>
- <input
- type="number"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder={t('common.stepCountPlaceholder')}
- defaultValue={editingItem?.steps || ''}
- />
- </div>
- <div className="col-span-2">
- <label className="block text-sm text-gray-700 mb-2">{t('common.remark')}</label>
- <textarea
- rows={3}
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm resize-none"
- placeholder={t('common.remarkPlaceholder')}
- defaultValue={editingItem?.remark || ''}
- ></textarea>
- </div>
- </div>
- );
- } else if (subMenu === 'SOP管理') {
- return (
- <div className="grid grid-cols-2 gap-4">
- <div>
- <label className="block text-sm text-gray-700 mb-2">SOP编号 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入SOP编号"
- defaultValue={editingItem?.code || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">SOP名称 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入SOP名称"
- defaultValue={editingItem?.name || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">版本号 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="如:V1.0"
- defaultValue={editingItem?.version || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">分类 *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">请选择</option>
- <option value="操作规程">操作规程</option>
- <option value="安全规程">安全规程</option>
- <option value="管理规程">管理规程</option>
- </select>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">所属部门 *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">请选择</option>
- <option value="技术部">技术部</option>
- <option value="运维部">运维部</option>
- <option value="安全部">安全部</option>
- </select>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">审核人</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入审核人"
- defaultValue={editingItem?.reviewer || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">生效日期</label>
- <input
- type="date"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- defaultValue={editingItem?.effectiveDate || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">复审日期</label>
- <input
- type="date"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- defaultValue={editingItem?.reviewDate || ''}
- />
- </div>
- <div className="col-span-2">
- <label className="block text-sm text-gray-700 mb-2">备注</label>
- <textarea
- rows={3}
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm resize-none"
- placeholder="请输入备注"
- defaultValue={editingItem?.remark || ''}
- ></textarea>
- </div>
- </div>
- );
- } else if (subMenu === '作业管理') {
- return (
- <div className="grid grid-cols-2 gap-4">
- <div>
- <label className="block text-sm text-gray-700 mb-2">作业编号 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="系统自动生成"
- defaultValue={editingItem?.code || ''}
- disabled
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">作业名称 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入作业名称"
- defaultValue={editingItem?.name || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">作业类型 *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">请选择</option>
- <option value="定期维护">定期维护</option>
- <option value="应急处理">应急处理</option>
- <option value="故障检修">故障检修</option>
- </select>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">流程模板 *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">请选择</option>
- <option value="高压设备隔离流程">高压设备隔离流程</option>
- <option value="低压配电柜隔离流程">低压配电柜隔离流程</option>
- <option value="紧急抢修隔离流程">紧急抢修隔离流程</option>
- </select>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">作业位置 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入作业位置"
- defaultValue={editingItem?.location || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">设备名称 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入设备名称"
- defaultValue={editingItem?.equipment || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">计划开始时间 *</label>
- <input
- type="datetime-local"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">计划结束时间 *</label>
- <input
- type="datetime-local"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">优先级</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="普通">普通</option>
- <option value="重要">重要</option>
- <option value="紧急">紧急</option>
- </select>
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">审批人</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">请选择</option>
- <option value="张三">张三</option>
- <option value="李四">李四</option>
- <option value="王五">王五</option>
- </select>
- </div>
- <div className="col-span-2">
- <label className="block text-sm text-gray-700 mb-2">作业说明</label>
- <textarea
- rows={3}
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm resize-none"
- placeholder="请输入作业说明"
- defaultValue={editingItem?.remark || ''}
- ></textarea>
- </div>
- </div>
- );
- }
- return null;
- };
- // 表单管理表格列配置
- const formManagementColumns: ColumnsType<TableRow> = useMemo(() => [
- {
- title: t('table.formId'),
- width: '5%',
- render: (_: any, __: TableRow, index: number) => index + 1,
- },
- {
- title: t('table.formName'),
- dataIndex: 'name',
- width: '20%',
- },
- {
- title: t('table.status'),
- dataIndex: 'status',
- width: '10%',
- render: (status: string) => (
- <span
- className={`inline-flex px-3 py-1 rounded-lg text-xs ${
- status === '启用' || status === t('common.enabled')
- ? 'bg-green-100 text-green-700'
- : 'bg-gray-100 text-gray-700'
- }`}
- >
- {status === '启用' ? t('common.enabled') : status === '停用' ? t('common.disabled') : status}
- </span>
- ),
- },
- {
- title: t('table.remark'),
- dataIndex: 'description',
- width: '25%',
- ellipsis: true,
- },
- {
- title: t('table.createTime'),
- dataIndex: 'createTime',
- width: '15%',
- },
- {
- title: t('table.operation'),
- width: '15%',
- align: 'center',
- render: (_: any, record: TableRow) => (
- <div className="flex items-center justify-center gap-2">
- <UIButton
- variant="ghost"
- size="sm"
- onClick={() => handleEdit(record)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-1">{t('common.edit')}</span>
- </UIButton>
- <UIButton
- variant="ghost"
- size="sm"
- onClick={() => handleDelete(record.id)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-1">{t('common.delete')}</span>
- </UIButton>
- </div>
- ),
- },
- ], [t, i18n.language]);
- // 流程设计表格列配置
- const processDesignColumns: ColumnsType<WorkflowDesignVO> = useMemo(() => [
- {
- title: t('table.processDesignId'),
- width: '5%',
- render: (_: any, __: WorkflowDesignVO, index: number) =>
- (processDesignPagination.pageNo - 1) * processDesignPagination.pageSize + index + 1,
- },
- {
- title: t('table.name') || '名称',
- dataIndex: 'name',
- width: '15%',
- },
- {
- title: t('table.nodeCount'),
- dataIndex: 'nodeCount',
- width: '10%',
- render: (count: number) => count ?? 0,
- },
- {
- title: t('table.status'),
- dataIndex: 'status',
- width: '10%',
- align: 'center',
- render: (status: number, record: WorkflowDesignVO) => {
- const isChecked = status === 1;
- const isUpdating = processDesignStatusUpdating[record.id!] || false;
- return (
- <AntdSwitch
- checked={isChecked}
- onChange={(checked) => {
- if (!isUpdating) {
- const newStatus = checked ? 1 : 0;
- handleProcessDesignStatusChanged(record, newStatus);
- }
- }}
- disabled={isUpdating}
- loading={isUpdating}
- style={{
- ...(isChecked ? {
- backgroundColor: '#52c41a',
- } : {})
- }}
- className={isChecked ? 'ant-switch-checked-green' : ''}
- />
- );
- },
- },
- {
- title: t('table.createTime'),
- dataIndex: 'createTime',
- width: '15%',
- render: (time: string | Date | number) => {
- if (!time) return '-';
- let date: Date;
- if (typeof time === 'number') {
- date = new Date(time);
- } else if (typeof time === 'string') {
- date = new Date(time);
- } else {
- date = time;
- }
- return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- });
- },
- },
- {
- title: t('table.updateTime'),
- dataIndex: 'updateTime',
- width: '15%',
- render: (time: string | Date | number) => {
- if (!time) return '-';
- let date: Date;
- if (typeof time === 'number') {
- date = new Date(time);
- } else if (typeof time === 'string') {
- date = new Date(time);
- } else {
- date = time;
- }
- return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- });
- },
- },
- {
- title: t('table.description'),
- dataIndex: 'description',
- width: '18%',
- render: (desc: string) => {
- const text = desc || '-';
- const maxLength = 20;
- const shouldTruncate = text.length > maxLength;
- const displayText = shouldTruncate ? text.slice(0, maxLength) + '...' : text;
-
- return (
- <Tooltip placement="topLeft" title={text}>
- <span>{displayText}</span>
- </Tooltip>
- );
- },
- },
- {
- title: t('table.operation'),
- width: '20%',
- align: 'center',
- render: (_: any, record: WorkflowDesignVO) => (
- <div className="flex items-center justify-center gap-2">
- <UIButton
- variant="ghost"
- size="sm"
- onClick={() => handleDesign(record as any)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Workflow className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-0.5">{t('common.design')}</span>
- </UIButton>
- <UIButton
- variant="ghost"
- size="sm"
- onClick={() => handleEdit(record as any)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-0.5">{t('common.edit')}</span>
- </UIButton>
- <UIButton
- variant="ghost"
- size="sm"
- onClick={() => handleDelete(record.id!)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-0.5">{t('common.delete')}</span>
- </UIButton>
- </div>
- ),
- },
- ], [t, i18n.language, processDesignPagination]);
- // 获取作业状态样式
- const getWorkJobStatusStyle = (status: string | number | undefined): React.CSSProperties => {
- if (!status) {
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- }
-
- // 从字典中查找状态文本
- const statusItem = jobStatusDictList.find(item => String(item.value) === String(status));
- const statusText = statusItem ? (statusItem.label || '') : String(status || '');
- const statusTextLower = statusText.toLowerCase();
-
- // 根据状态文本判断颜色
- // 待执行、待发布:灰色 #e5e5e5
- if (statusTextLower.includes('待执行') || statusTextLower.includes('待发布') ||
- statusTextLower.includes('pending') || statusTextLower.includes('unreleased')) {
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- }
- // 进行中:蓝色 #1677ff
- if (statusTextLower.includes('进行中') || statusTextLower.includes('执行中') ||
- statusTextLower.includes('running') || statusTextLower.includes('in_progress')) {
- return {
- backgroundColor: '#1677ff',
- color: '#ffffff',
- };
- }
- // 已完成:绿色 #0acb57
- if (statusTextLower.includes('已完成') || statusTextLower.includes('执行完成') ||
- statusTextLower.includes('completed') || statusTextLower.includes('完成')) {
- return {
- backgroundColor: '#0acb57',
- color: '#ffffff',
- };
- }
-
- // 如果没有匹配到,默认灰色
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- };
- // 获取紧急程度图标和样式(根据字典 value 判断:0=一般,1=紧急,2=非常紧急)
- const getUrgencyLevelIconAndStyle = (urgencyValue: string | number | undefined): { icon: React.ReactNode; style: React.CSSProperties } => {
- if (urgencyValue === null || urgencyValue === undefined || urgencyValue === '') {
- return {
- icon: null,
- style: {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- },
- };
- }
-
- const value = Number(urgencyValue);
-
- // 0 = 一般:使用 urgecy1.png 图标 + 黑色文字
- if (value === 0) {
- return {
- icon: (
- <img
- src={urgecy1Icon}
- alt="一般"
- className="w-5 h-5 flex-shrink-0 mr-1.5"
- style={{ objectFit: 'contain' }}
- />
- ),
- style: {
- backgroundColor: 'transparent',
- color: '#000000',
- },
- };
- }
-
- // 1 = 紧急:使用 urgecy2.png 图标 + 橙色加粗文字
- if (value === 1) {
- return {
- icon: (
- <img
- src={urgecy2Icon}
- alt="紧急"
- className="w-5 h-5 flex-shrink-0 mr-1.5"
- style={{ objectFit: 'contain' }}
- />
- ),
- style: {
- backgroundColor: 'transparent',
- color: '#fa8c16',
- fontWeight: 'bold',
- },
- };
- }
-
- // 2 = 非常紧急:使用 urgecy3.png 图标 + 红色加粗文字
- if (value === 2) {
- return {
- icon: (
- <img
- src={urgecy3Icon}
- alt="非常紧急"
- className="w-5 h-5 flex-shrink-0 mr-1.5"
- style={{ objectFit: 'contain' }}
- />
- ),
- style: {
- backgroundColor: 'transparent',
- color: '#ff4d4f',
- fontWeight: 'bold',
- },
- };
- }
-
- // 如果没有匹配到,默认灰色
- return {
- icon: null,
- style: {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- },
- };
- };
- // 获取紧急程度样式(保留用于向后兼容)
- const getUrgencyLevelStyle = (urgencyValue: string | number | undefined): React.CSSProperties => {
- return getUrgencyLevelIconAndStyle(urgencyValue).style;
- };
- // 作业管理表格列配置
- const workJobColumns: ColumnsType<WorkJobVO> = useMemo(() => [
- {
- title: t('table.workOrderNo'),
- dataIndex: 'orderNo',
- width: '12%',
- render: (orderNo: string, record: WorkJobVO) => orderNo || record.code || '-',
- },
- {
- title: t('table.workName'),
- dataIndex: 'name',
- width: '18%',
- render: (name: string, record: WorkJobVO) => {
- if (!name || name === '-') return <span>{name || '-'}</span>;
- return (
- <span
- className="text-blue-600 cursor-pointer hover:text-blue-800 hover:underline"
- onClick={(e) => {
- e.stopPropagation();
- handleWorkJobView(record);
- }}
- >
- {name}
- </span>
- );
- },
- },
- {
- title: t('table.workStatus'),
- dataIndex: 'status',
- width: '10%',
- render: (status: string | number) => {
- const statusItem = jobStatusDictList.find(item => String(item.value) === String(status));
- const statusText = statusItem ? (statusItem.label || '') : String(status || '-');
- return (
- <span
- className="inline-flex px-3 py-1 rounded-lg text-xs"
- style={getWorkJobStatusStyle(status)}
- >
- {statusText}
- </span>
- );
- },
- },
- {
- title: t('table.currentTask'),
- dataIndex: 'currentNodeName',
- width: '12%',
- render: (currentNodeName: string, record: WorkJobVO) => currentNodeName || record.currentNode || '-',
- },
- {
- title: t('table.workContent'),
- dataIndex: 'description',
- width: '20%',
- render: (description: string, record: WorkJobVO) => {
- const content = description || record.content || '-';
- if (content === '-') return content;
-
- const maxLength = 20;
- const shouldTruncate = content.length > maxLength;
- const displayText = shouldTruncate ? content.slice(0, maxLength) + '...' : content;
-
- return (
- <Tooltip placement="topLeft" title={content}>
- <span>{displayText}</span>
- </Tooltip>
- );
- },
- },
- {
- title: t('table.initiator'),
- dataIndex: 'initiatorName',
- width: '10%',
- render: (initiatorName: string, record: WorkJobVO) => initiatorName || record.initiator || '-',
- },
- {
- title: t('table.initiationTime'),
- dataIndex: 'initiationTime',
- width: '16%',
- render: (time: string | Date | number, record: WorkJobVO) => {
- const actualTime = time || record.initiateTime;
- if (!actualTime) return '-';
- let date: Date;
- if (typeof actualTime === 'number') {
- date = new Date(actualTime);
- } else if (typeof actualTime === 'string') {
- date = new Date(actualTime);
- } else {
- date = actualTime;
- }
- return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- });
- },
- },
- {
- title: t('table.urgencyLevel'),
- dataIndex: 'urgencyLevel',
- minWidth: '180px',
- align: 'center',
- render: (urgencyLevel: string | number | undefined, record: WorkJobVO) => {
- const urgencyValue = urgencyLevel || record.urgency || record.urgencyLevel;
- const urgencyItem = urgencyLevelDictList.find(item => String(item.value) === String(urgencyValue));
- const urgencyText = urgencyItem ? (urgencyItem.label || '') : (urgencyValue ? String(urgencyValue) : '-');
-
- if (!urgencyValue || urgencyValue === null || urgencyValue === undefined || urgencyValue === '') {
- return <span>-</span>;
- }
-
- const { icon, style } = getUrgencyLevelIconAndStyle(urgencyValue);
-
- return (
- <span
- className="inline-flex items-center justify-center gap-1.5"
- >
- {icon}
- <span style={style}>{urgencyText}</span>
- </span>
- );
- },
- },
- {
- title: t('table.operation'),
- width: '20%',
- align: 'center',
- fixed: 'right',
- render: (_: any, record: WorkJobVO) => {
- // 只有未发布状态(unreleased)才能编辑
- const canEdit = String(record.status).toLowerCase() === 'unreleased';
-
- return (
- <div className="flex items-center justify-center gap-2">
- <UIButton
- variant="ghost"
- size="sm"
- onClick={(e) => {
- e.stopPropagation();
- e.preventDefault();
- handleWorkJobView(record);
- }}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Eye className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-0.5">{t('table.view')}</span>
- </UIButton>
- {canEdit ? (
- <UIButton
- variant="ghost"
- size="sm"
- onClick={() => handleWorkJobEdit(record)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-0.5">{t('common.edit')}</span>
- </UIButton>
- ) : (
- <UIButton
- variant="ghost"
- size="sm"
- disabled
- className="h-8 px-2 opacity-50 cursor-not-allowed"
- title={t('common.editDisabled') || '只有未发布状态才能编辑'}
- >
- <Edit2 className="w-4 h-4" />
- <span className="ml-0.5">{t('common.edit')}</span>
- </UIButton>
- )}
- <UIButton
- variant="ghost"
- size="sm"
- onClick={() => handleWorkJobDelete(record.id!)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.style.textDecoration = 'underline';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.style.textDecoration = 'none';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-0.5">{t('common.delete')}</span>
- </UIButton>
- </div>
- );
- },
- },
- ], [t, i18n.language, jobStatusDictList, urgencyLevelDictList]);
- // 如果是表单管理页面,使用特殊的布局
- if (subMenu === '表单管理') {
- // 计算分页数据
- const formManagementTotal = filteredData.length;
- const formManagementStartIndex = (formManagementPagination.pageNo - 1) * formManagementPagination.pageSize;
- const formManagementEndIndex = formManagementStartIndex + formManagementPagination.pageSize;
- const formManagementPageData = filteredData.slice(formManagementStartIndex, formManagementEndIndex);
- const formManagementTotalPages = Math.ceil(formManagementTotal / formManagementPagination.pageSize) || 1;
- return (
- <div className="space-y-6">
- {/* 搜索栏和表格容器 */}
- <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
- {/* 搜索栏 */}
- <div className="p-4 lg:p-5 border-b border-gray-200/50">
- <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
- {/* 搜索输入框 */}
- <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
- <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.formName')}:</label>
- <Input
- value={formManagementQuery.name}
- onChange={(e) => setFormManagementQuery({ ...formManagementQuery, name: e.target.value })}
- onPressEnter={handleFormManagementQuery}
- placeholder={t('form.formNamePlaceholder')}
- className="min-w-[150px] max-w-[200px]"
- allowClear
- />
- </div>
- <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.status')}:</label>
- <Select
- value={formManagementQuery.status}
- onChange={(value) => setFormManagementQuery({ ...formManagementQuery, status: value })}
- placeholder={t('form.statusPlaceholder') || t('common.pleaseSelect')}
- className="min-w-[150px] max-w-[200px]"
- allowClear
- >
- <Select.Option value="启用">{t('common.enabled')}</Select.Option>
- <Select.Option value="停用">{t('common.disabled')}</Select.Option>
- </Select>
- </div>
- </div>
- {/* 操作按钮组 */}
- <Space className="flex-shrink-0">
- <Button
- type="primary"
- icon={<Search className="w-4 h-4" />}
- onClick={handleFormManagementQuery}
- >
- {t('common.search')}
- </Button>
-
- <Button
- icon={<RefreshCw className="w-4 h-4" />}
- onClick={() => {
- resetFormManagementQuery();
- setFormManagementPagination({ pageNo: 1, pageSize: 10 });
- }}
- >
- {t('common.reset')}
- </Button>
-
- <Button
- type="primary"
- icon={<Plus className="w-4 h-4" />}
- onClick={() => {
- setEditingItem(null);
- setShowAddModal(true);
- }}
- >
- {t('form.addForm')}
- </Button>
- </Space>
- </div>
- </div>
- {/* 表格容器 */}
- <div className="overflow-hidden min-w-0">
- <AntdTable
- columns={formManagementColumns}
- dataSource={formManagementPageData}
- rowKey="id"
- pagination={false}
- scroll={{ x: 'max-content' }}
- />
- </div>
- </div>
- {/* 分页 */}
- {formManagementTotal > 0 && (
- <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
- <div className="flex items-center justify-between">
- <div className="text-sm text-gray-600">
- {t('common.total')} <span className="text-blue-600 font-medium">{formManagementTotal}</span> {t('common.records')}
- </div>
- <div className="flex gap-2">
- <Button
- onClick={() => setFormManagementPagination({ ...formManagementPagination, pageNo: formManagementPagination.pageNo - 1 })}
- disabled={formManagementPagination.pageNo <= 1}
- >
- {t('common.prevPage')}
- </Button>
- <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
- {formManagementPagination.pageNo} / {formManagementTotalPages}
- </span>
- <Button
- onClick={() => setFormManagementPagination({ ...formManagementPagination, pageNo: formManagementPagination.pageNo + 1 })}
- disabled={formManagementPagination.pageNo >= formManagementTotalPages}
- >
- {t('common.nextPage')}
- </Button>
- </div>
- </div>
- </div>
- )}
- {/* 新增/编辑弹窗 */}
- {showAddModal && (
- <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
- <div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto animate-in zoom-in duration-200">
- <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between sticky top-0 bg-white z-10">
- <h3 className="text-lg text-gray-900">
- {editingItem ? t('common.editFormManagement') : t('common.addFormManagement')}
- </h3>
- <button
- onClick={() => setShowAddModal(false)}
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
- >
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
- </svg>
- </button>
- </div>
- <div className="px-6 py-6">
- <div className="grid grid-cols-2 gap-4">
- <div>
- <label className="block text-sm text-gray-700 mb-2">名称 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入名称"
- defaultValue={editingItem?.name || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">创建人 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入创建人"
- defaultValue={editingItem?.creator || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">版本</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="如:V1.0"
- defaultValue={editingItem?.version || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">状态 *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">请选择</option>
- <option value="启用">启用</option>
- <option value="停用">停用</option>
- </select>
- </div>
- <div className="col-span-2">
- <label className="block text-sm text-gray-700 mb-2">{t('common.description')}</label>
- <textarea
- rows={3}
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm resize-none"
- placeholder={t('common.descriptionPlaceholder')}
- defaultValue={editingItem?.description || ''}
- ></textarea>
- </div>
- </div>
- </div>
- <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
- <button
- onClick={() => setShowAddModal(false)}
- className="px-5 py-2.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
- >
- 取消
- </button>
- <button
- onClick={() => {
- console.log('保存:', editingItem);
- setShowAddModal(false);
- }}
- className="px-5 py-2.5 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all"
- >
- {editingItem ? '保存修改' : '确定'}
- </button>
- </div>
- </div>
- </div>
- )}
- </div>
- );
- }
- // 如果是流程设计页面,使用特殊的布局
- if (subMenu === '流程设计') {
- // 使用接口返回的数据
- const processDesignPageData = processDesignList;
- const processDesignTotalPages = Math.ceil(processDesignTotal / processDesignPagination.pageSize) || 1;
- return (
- <div className="space-y-6">
- {/* 搜索栏和表格容器 */}
- <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
- {/* 搜索栏 */}
- <div className="p-4 lg:p-5 border-b border-gray-200/50">
- <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
- {/* 搜索输入框 */}
- <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
- <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.processDesignName')}:</label>
- <Input
- value={processDesignQuery.name}
- onChange={(e) => setProcessDesignQuery({ ...processDesignQuery, name: e.target.value })}
- onPressEnter={handleProcessDesignQuery}
- placeholder={t('form.processDesignNamePlaceholder')}
- className="min-w-[150px] max-w-[200px]"
- allowClear
- />
- </div>
- <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
- </div>
- </div>
- {/* 操作按钮组 */}
- <Space className="flex-shrink-0">
- <Button
- type="primary"
- icon={<Search className="w-4 h-4" />}
- onClick={handleProcessDesignQuery}
- >
- {t('common.search')}
- </Button>
-
- <Button
- icon={<RefreshCw className="w-4 h-4" />}
- onClick={() => {
- resetProcessDesignQuery();
- }}
- >
- {t('common.reset')}
- </Button>
-
- <Button
- type="primary"
- icon={<Plus className="w-4 h-4" />}
- onClick={() => {
- setEditingItem(null);
- form.resetFields();
- setShowAddModal(true);
- }}
- >
- {t('form.addProcess')}
- </Button>
- </Space>
- </div>
- </div>
- {/* 表格容器 */}
- <div className="overflow-hidden min-w-0">
- <AntdTable
- columns={processDesignColumns}
- dataSource={processDesignPageData}
- rowKey="id"
- pagination={false}
- scroll={{ x: 'max-content' }}
- loading={processDesignLoading}
- />
- </div>
- </div>
- {/* 分页 */}
- {processDesignTotal > 0 && (
- <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
- <div className="flex items-center justify-between">
- <div className="text-sm text-gray-600">
- {t('common.total')} <span className="text-blue-600 font-medium">{processDesignTotal}</span> {t('common.records')}
- </div>
- <div className="flex gap-2">
- <Button
- onClick={() => {
- setProcessDesignPagination({ ...processDesignPagination, pageNo: processDesignPagination.pageNo - 1 });
- }}
- disabled={processDesignPagination.pageNo <= 1}
- >
- {t('common.prevPage')}
- </Button>
- <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
- {processDesignPagination.pageNo} / {processDesignTotalPages}
- </span>
- <Button
- onClick={() => {
- setProcessDesignPagination({ ...processDesignPagination, pageNo: processDesignPagination.pageNo + 1 });
- }}
- disabled={processDesignPagination.pageNo >= processDesignTotalPages}
- >
- {t('common.nextPage')}
- </Button>
- </div>
- </div>
- </div>
- )}
- {/* 新增/编辑弹窗 */}
- {subMenu === '流程设计' ? (
- <Modal
- title={editingItem ? t('common.editProcessDesign') : t('common.addProcessDesign')}
- open={showAddModal}
- onCancel={() => {
- setShowAddModal(false);
- setEditingItem(null);
- form.resetFields();
- }}
- onOk={async () => {
- try {
- const values = await form.validateFields();
- if (editingItem) {
- // 编辑模式:调用更新接口,只更新name和description,content不变
- await workflowDesignApi.updateWorkflowDesign({
- id: editingItem.id!,
- name: values.name,
- description: values.description || '',
- });
- message.success(t('common.updateProcessSuccess'));
- setShowAddModal(false);
- setEditingItem(null);
- form.resetFields();
- getProcessDesignList();
- } else {
- // 新建模式:调用新建接口
- const result = await workflowDesignApi.insertWorkflowDesign({
- name: values.name,
- description: values.description || '',
- });
-
- // API返回的是流程ID(数字),不是对象
- const workflowId = typeof result === 'number' ? result : result?.id;
-
- // 关闭新建弹窗
- setShowAddModal(false);
- setEditingItem(null);
- form.resetFields();
- getProcessDesignList();
-
- // 显示确认框
- if (workflowId) {
- Modal.confirm({
- title: t('common.processDesignComplete'),
- okText: t('common.designNow'),
- cancelText: t('common.later'),
- onOk: () => {
- // 直接跳转到流程设计页面,与设计按钮逻辑一致
- navigate(`/process-designer?id=${workflowId}`);
- },
- });
- } else {
- // 如果没有ID,直接显示成功消息
- message.success(t('common.addProcessSuccess'));
- }
- }
- } catch (error: any) {
- if (error.errorFields) {
- // 表单验证错误
- return;
- }
- console.error(editingItem ? '更新流程失败:' : '新建流程失败:', error);
- message.error(error?.message || (editingItem ? t('common.updateProcessFailed') : t('common.addProcessFailed')));
- }
- }}
- okText={t('common.confirm')}
- cancelText={t('common.cancel')}
- width={700}
- destroyOnClose
- >
- <Form
- form={form}
- labelCol={{ span: 5 }}
- wrapperCol={{ span: 19 }}
- key={editingItem?.id || 'new'} // 使用key确保编辑时重新初始化表单
- >
- <Form.Item
- label={t('common.processName')}
- name="name"
- rules={[{ required: true, message: t('common.processNameRequired') }]}
- >
- <Input placeholder={t('common.processNamePlaceholder')} />
- </Form.Item>
- <Form.Item
- label={t('common.description')}
- name="description"
- style={{ marginBottom: '24px' }}
- >
- <Input.TextArea rows={7} placeholder={t('common.descriptionPlaceholder')} />
- </Form.Item>
- </Form>
- </Modal>
- ) : showAddModal && (
- <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
- <div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto animate-in zoom-in duration-200">
- <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between sticky top-0 bg-white z-10">
- <h3 className="text-lg text-gray-900">
- {editingItem ? t('common.editProcessDesign') : t('common.addProcessDesign')}
- </h3>
- <button
- onClick={() => setShowAddModal(false)}
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
- >
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
- </svg>
- </button>
- </div>
- <div className="px-6 py-6">
- {(
- // 其他菜单的表单保持原样
- <div className="grid grid-cols-2 gap-4">
- <div>
- <label className="block text-sm text-gray-700 mb-2">名称 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入名称"
- defaultValue={editingItem?.name || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">设计人 *</label>
- <input
- type="text"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入设计人"
- defaultValue={editingItem?.designer || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">任务数量</label>
- <input
- type="number"
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- placeholder="请输入任务数量"
- defaultValue={editingItem?.nodeCount || ''}
- />
- </div>
- <div>
- <label className="block text-sm text-gray-700 mb-2">状态 *</label>
- <select className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm">
- <option value="">请选择</option>
- <option value="启用">启用</option>
- <option value="停用">停用</option>
- </select>
- </div>
- <div className="col-span-2">
- <label className="block text-sm text-gray-700 mb-2">{t('common.description')}</label>
- <textarea
- rows={3}
- className="w-full px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm resize-none"
- placeholder={t('common.descriptionPlaceholder')}
- defaultValue={editingItem?.description || ''}
- ></textarea>
- </div>
- </div>
- )}
- </div>
- <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
- <button
- onClick={() => setShowAddModal(false)}
- className="px-5 py-2.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
- >
- 取消
- </button>
- <button
- onClick={() => {
- // 其他菜单的保存逻辑
- console.log('保存:', editingItem);
- setShowAddModal(false);
- }}
- className="px-5 py-2.5 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all"
- >
- {editingItem ? '保存修改' : '确定'}
- </button>
- </div>
- </div>
- </div>
- )}
- </div>
- );
- }
- // 如果是作业管理页面,使用特殊的布局
- if (subMenu === '作业管理') {
- const workJobPageData = workJobList;
- return (
- <div className="space-y-6">
- {/* 搜索栏和表格容器 */}
- <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
- {/* 搜索栏 */}
- <div className="p-4 lg:p-5 border-b border-gray-200/50">
- <div className="flex flex-wrap items-center justify-between gap-3 lg:gap-4">
- <div className="flex flex-wrap items-center gap-3 lg:gap-4">
- <div className="flex items-center gap-2">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.workName')}:</label>
- <Input
- value={workJobQuery.name}
- onChange={(e) => setWorkJobQuery({ ...workJobQuery, name: e.target.value })}
- onPressEnter={handleWorkJobQuery}
- placeholder={t('form.workNamePlaceholder')}
- allowClear
- className="min-w-[200px] max-w-[300px]"
- />
- </div>
- <div className="flex items-center gap-2">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.workStatus')}:</label>
- <Select
- value={workJobQuery.status ?? undefined}
- onChange={(value) => setWorkJobQuery({ ...workJobQuery, status: value || undefined })}
- placeholder={t('form.workStatusPlaceholder')}
- allowClear
- className="w-32"
- >
- {jobStatusDictList.map(item => (
- <Select.Option key={item.value} value={item.value}>
- {item.label}
- </Select.Option>
- ))}
- </Select>
- </div>
- </div>
- <Space>
- <Button
- type="primary"
- icon={<Search className="w-4 h-4" />}
- onClick={handleWorkJobQuery}
- >
- {t('common.search')}
- </Button>
- <Button
- icon={<RefreshCw className="w-4 h-4" />}
- onClick={() => {
- resetWorkJobQuery();
- getWorkJobList();
- }}
- >
- {t('common.reset')}
- </Button>
- <Button
- type="primary"
- icon={<Plus className="w-4 h-4" />}
- onClick={() => {
- setEditingItem(null);
- setShowAddModal(true);
- setOriginalBasicFormData(null); // 清空原始表单数据(新增模式)
- // 打开弹框时加载数据
- getWorkTypeDictList();
- getUrgencyLevelDictList();
- getIsolationMethodDictList();
- getWorkflowTemplateList();
- }}
- >
- {t('form.initiateWork')}
- </Button>
- </Space>
- </div>
- </div>
- {/* 表格容器 */}
- <div className="overflow-hidden min-w-0">
- <AntdTable
- columns={workJobColumns}
- dataSource={workJobPageData}
- rowKey="id"
- pagination={false}
- scroll={{ x: 'max-content' }}
- loading={workJobLoading}
- />
- </div>
- </div>
- {/* 分页 */}
- {workJobTotal > 0 && (
- <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
- <div className="flex items-center justify-between">
- <div className="text-sm text-gray-600">
- {t('common.total')} <span className="text-blue-600 font-medium">{workJobTotal}</span> {t('common.records')}
- </div>
- <div className="flex gap-2">
- <Button
- onClick={() => setWorkJobPagination({ ...workJobPagination, pageNo: workJobPagination.pageNo - 1 })}
- disabled={workJobPagination.pageNo <= 1}
- >
- {t('common.prevPage')}
- </Button>
- <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
- {workJobPagination.pageNo} / {Math.ceil(workJobTotal / workJobPagination.pageSize) || 1}
- </span>
- <Button
- onClick={() => setWorkJobPagination({ ...workJobPagination, pageNo: workJobPagination.pageNo + 1 })}
- disabled={workJobPagination.pageNo >= Math.ceil(workJobTotal / workJobPagination.pageSize)}
- >
- {t('common.nextPage')}
- </Button>
- </div>
- </div>
- </div>
- )}
- {/* 发起作业多步骤弹窗(新增和编辑共用) */}
- {subMenu === '作业管理' && showAddModal && !editingItem ? (
- <Modal
- title={null}
- open={showAddModal}
- onCancel={() => {
- setShowAddModal(false);
- setWorkJobStep(0);
- workJobBasicForm.resetFields();
- workJobPublishForm.resetFields();
- setWorkflowJson(null);
- setWorkflowWorkId(null); // 清空作业ID
- setIsViewMode(false); // 重置查看模式
- // 清空流程管理相关状态
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setSelectedWorkflowNode(null);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- setNodeSavedStatusCache(new Map());
- setWorkflowWorkNodeDOList([]); // 清空节点数据列表
- setOriginalBasicFormData(null); // 清空原始表单数据
- // 关闭弹框时刷新列表
- getWorkJobList();
- }}
- footer={null}
- width={1400}
- destroyOnClose
- styles={{
- body: { padding: 0 }
- }}
- >
- <div className="flex flex-col h-[80vh]">
- {/* Tab导航 */}
- <div className="px-6 py-4 bg-white">
- <Tabs
- activeKey={String(workJobStep)}
- onChange={(key) => {
- // 查看模式下可以自由切换tab
- if (isViewMode) {
- setWorkJobStep(Number(key));
- return;
- }
- // 只有第一个tab完成才能进入第二个tab
- if (key === '1' && workJobStep === 0) {
- workJobBasicForm.validateFields().then(() => {
- setWorkJobStep(1);
- }).catch(() => {
- message.warning(t('common.pleaseCompleteBasicInfo'));
- });
- } else if (key === '2' && workJobStep < 2) {
- // 只有第二个tab完成才能进入第三个tab
- if (workflowJson) {
- setWorkJobStep(2);
- } else {
- message.warning(t('common.pleaseCompleteWorkflowManagement'));
- }
- } else {
- setWorkJobStep(Number(key));
- }
- }}
- items={[
- {
- key: '0',
- label: (
- <span className="flex items-center gap-2">
- <FileText className="w-4 h-4" />
- {t('common.basicInfo')}
- </span>
- ),
- },
- {
- key: '1',
- label: (
- <span className="flex items-center gap-2">
- <Workflow className="w-4 h-4" />
- {t('common.workflowManagement')}
- </span>
- ),
- disabled: isViewMode ? false : workJobStep < 1,
- },
- {
- key: '2',
- label: (
- <span className="flex items-center gap-2">
- <Play className="w-4 h-4" />
- {t('common.publishWork')}
- </span>
- ),
- disabled: isViewMode ? false : workJobStep < 2,
- },
- ]}
- />
- </div>
- {/* Tab内容 */}
- <div className="flex-1 overflow-y-auto px-6 py-4 bg-white">
- {workJobStep === 0 && (
- <div className="flex justify-center">
- <Form
- form={workJobBasicForm}
- layout="vertical"
- className="max-w-2xl w-full"
- >
- <Form.Item
- label={t('common.workflowTemplate')}
- name="workflowTemplate"
- required
- rules={[{ required: true, message: t('common.workflowTemplateRequired') }]}
- help={t('common.workflowTemplateHelp')}
- >
- <Select
- placeholder={t('common.workflowTemplatePlaceholder')}
- allowClear
- showSearch
- disabled={isViewMode}
- filterOption={(input, option) =>
- (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
- }
- options={workflowTemplateList
- .filter(item => item.status === 1) // 只显示启用的
- .map(item => ({
- label: item.name,
- value: item.id,
- }))}
- onChange={(value) => {
- if (value) {
- // 加载流程设计JSON
- loadWorkflowJson(value);
- // 自动填充作业名称为流程模板名称
- const selectedTemplate = workflowTemplateList.find(item => item.id === value);
- if (selectedTemplate?.name) {
- workJobBasicForm.setFieldsValue({ jobName: selectedTemplate.name });
- }
- } else {
- setWorkflowJson(null);
- // 清空作业名称
- workJobBasicForm.setFieldsValue({ jobName: '' });
- }
- }}
- />
- </Form.Item>
- <Form.Item
- label={t('common.jobCategory')}
- name="jobCategory"
- required
- rules={[{ required: true, message: t('common.jobCategoryRequired') }]}
- >
- <Select placeholder={t('common.jobCategoryPlaceholder')} allowClear disabled={isViewMode}>
- {workTypeDictList.map((item) => (
- <Select.Option key={item.id} value={item.value}>
- {item.label}
- </Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item
- label={t('common.workName')}
- name="jobName"
- required
- rules={[{ required: true, message: t('common.workNameRequired') }]}
- >
- <Input placeholder={t('common.workNamePlaceholder')} disabled={isViewMode} readOnly={isViewMode} />
- </Form.Item>
- <Form.Item
- label={t('common.workContent')}
- name="jobContent"
- required
- rules={[{ required: true, message: t('common.workContentPlaceholder') }]}
- >
- <Input.TextArea
- rows={4}
- placeholder={t('common.workContentPlaceholder')}
- showCount
- maxLength={500}
- disabled={isViewMode}
- readOnly={isViewMode}
- />
- </Form.Item>
- <Form.Item
- label={t('common.urgencyLevel')}
- name="urgency"
- >
- <Radio.Group disabled={isViewMode}>
- {urgencyLevelDictList.length > 0 ? (
- urgencyLevelDictList.map((item) => (
- <Radio key={item.id} value={item.value}>
- {item.label}
- </Radio>
- ))
- ) : (
- <span className="text-gray-400 text-sm">{t('common.loading')}</span>
- )}
- </Radio.Group>
- </Form.Item>
- </Form>
- </div>
- )}
- {workJobStep === 1 && (
- <div className="flex gap-4" style={{ height: 'calc(80vh - 250px)' }}>
- {/* 左侧:流程设计渲染区域(面积大一些) */}
- <div className="flex-1 border border-gray-200 rounded-lg overflow-hidden bg-gray-50" style={{ minWidth: 0 }}>
- {workflowNodes.length > 0 ? (
- <div className="h-full" ref={workflowReactFlowWrapper}>
- <style>{`
- .react-flow__arrowhead {
- fill: #000000 !important;
- }
- .react-flow__edge-path {
- stroke: #000000 !important;
- }
- .react-flow__edge marker path {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- .react-flow__edge path {
- stroke: #000000 !important;
- }
- svg marker path {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- svg marker polygon {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- .react-flow__edge .react-flow__edge-path {
- stroke: #000000 !important;
- }
- .react-flow__edge svg path {
- stroke: #000000 !important;
- }
- .react-flow__edge svg marker polygon {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- .react-flow__edge svg marker path {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- .react-flow__edge svg marker {
- fill: #000000 !important;
- }
- svg defs marker path {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- svg defs marker polygon {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- .react-flow__edge svg defs marker path {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- .react-flow__edge svg defs marker polygon {
- fill: #000000 !important;
- stroke: #000000 !important;
- }
- `}</style>
- <ReactFlow
- nodes={workflowNodes}
- edges={workflowEdges}
- onNodesChange={isViewMode ? undefined : onWorkflowNodesChange}
- onEdgesChange={isViewMode ? undefined : onWorkflowEdgesChange}
- onNodeClick={isViewMode ? undefined : onWorkflowNodeClick}
- onPaneClick={isViewMode ? undefined : onWorkflowPaneClick}
- nodesDraggable={!isViewMode}
- nodesConnectable={!isViewMode}
- elementsSelectable={!isViewMode}
- nodeTypes={nodeTypes}
- edgeTypes={edgeTypes}
- fitView
- onInit={(instance) => {
- setWorkflowReactFlowInstance(instance);
- // 初始化后自动适应视图
- setTimeout(() => {
- instance.fitView({ padding: 0.2 });
- }, 100);
- }}
- connectionMode={ConnectionMode.Loose}
- defaultEdgeOptions={{
- style: { strokeWidth: 2, stroke: '#000000' },
- type: 'straight',
- markerStart: {
- type: 'arrowclosed',
- color: '#000000',
- },
- }}
- edgesUpdatable={!isViewMode}
- >
- <Controls className="!bg-white !border !border-gray-200 !rounded-lg !shadow-md" />
- <Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#e5e7eb" />
- </ReactFlow>
- </div>
- ) : (
- <div className="flex items-center justify-center h-full text-gray-400">
- {workflowJson ? '正在加载流程设计...' : '请先在基本信息中选择流程模板'}
- </div>
- )}
- </div>
- {/* 右侧:配置面板 */}
- <div className="w-80 border border-gray-200 rounded-lg bg-white overflow-y-auto flex-shrink-0">
- {selectedWorkflowNode ? (
- <div className="h-full flex flex-col">
- {/* 头部 */}
- <div className="px-4 py-2.5 bg-white border-b border-gray-200 flex items-center justify-between sticky top-0 z-10 shadow-sm">
- <h3 className="text-sm font-medium text-gray-900">
- {workflowNodeConfig.nodeName || selectedWorkflowNode.data?.label || '节点'} 设置
- </h3>
- <button
- onClick={() => setSelectedWorkflowNode(null)}
- className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
- disabled={isViewMode}
- >
- <CloseOutlined className="text-gray-500" />
- </button>
- </div>
- {/* 内容 */}
- <div className="flex-1 p-4 overflow-y-auto space-y-6">
- {/* 节点信息 */}
- <div>
- <h4 className="flex items-center gap-3 text-base font-semibold text-gray-900 mb-4">
- <span className="w-1 h-5 bg-blue-500 rounded-full flex-shrink-0" style={{ minWidth: '4px', minHeight: '20px' }}></span>
- 节点信息
- </h4>
- <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>
- <Input
- value={workflowNodeConfig.nodeName || ''}
- onChange={(e) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, nodeName: e.target.value })
- }
- placeholder="请输入节点名称"
- className="rounded-lg border-gray-200"
- disabled={isViewMode}
- />
- </div>
- {/* 负责人 */}
- {selectedWorkflowNode.data?.type !== 'createJob' && selectedWorkflowNode.data?.type !== 'isolation' && selectedWorkflowNode.data?.type !== 'releaseIsolation' && (
- <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={workflowNodeConfig.responsible || undefined}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, responsible: value || undefined })
- }
- placeholder="请选择负责人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={isViewMode}
- >
- {workflowDrawerUsers.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>
- )}
- {/* 备注 */}
- {selectedWorkflowNode.data?.type !== 'createJob' && selectedWorkflowNode.data?.type !== 'confirm' && selectedWorkflowNode.data?.type !== 'review' && selectedWorkflowNode.data?.type !== 'inputInfo' && selectedWorkflowNode.data?.type !== 'isolation' && selectedWorkflowNode.data?.type !== 'releaseIsolation' && selectedWorkflowNode.data?.type !== 'returnLock' && selectedWorkflowNode.data?.type !== 'complete' && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 备注
- </label>
- <Input.TextArea
- value={workflowNodeConfig.remark || ''}
- onChange={(e) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, remark: e.target.value })
- }
- placeholder="请输入备注"
- rows={3}
- className="rounded-lg border-gray-200"
- disabled={isViewMode}
- />
- </div>
- )}
- </div>
- </div>
- {/* 提交表单 - 创建作业节点不显示 */}
- {selectedWorkflowNode.data?.type !== 'createJob' && (
- <div>
- <h4 className="flex items-center gap-3 text-base font-semibold text-gray-900 mb-4">
- <span className="w-1 h-5 bg-blue-500 rounded-full flex-shrink-0" style={{ minWidth: '4px', minHeight: '20px' }}></span>
- 提交表单
- </h4>
- <div className="space-y-5">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 业务表单
- {/* 只有确认节点、审核节点、录入信息节点显示必填标记 */}
- {['confirm', 'review', 'inputInfo'].includes(selectedWorkflowNode.data?.type || '') && (
- <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
- )}
- </label>
- <Select
- value={workflowNodeConfig.submitForm || undefined}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, submitForm: value || undefined })
- }
- placeholder="请选择提交表单"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={isViewMode}
- >
- {workflowFormList.map(form => (
- <Select.Option key={form.id} value={form.id}>{form.name}</Select.Option>
- ))}
- </Select>
- </div>
- {/* 隔离/方案 和 解除隔离 节点特有的字段 */}
- {(selectedWorkflowNode.data?.type === 'isolation' || selectedWorkflowNode.data?.type === 'releaseIsolation') && (
- <>
- {/* 解除隔离节点:选择隔离节点 */}
- {selectedWorkflowNode.data?.type === 'releaseIsolation' && (
- <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={workflowNodeConfig.isolationNodeUuid || undefined}
- disabled={isViewMode}
- onChange={(value) => {
- setWorkflowNodeConfig((prev) => {
- if (!value) {
- return { ...prev, isolationNodeUuid: '' };
- }
-
- // 从 workflowWorkNodeDOList 中查找对应的隔离节点数据
- const isolationNodeDO = workflowWorkNodeDOList.find(item => item.uuid === value && item.type === 'isolation');
-
- if (isolationNodeDO) {
- // 解析隔离节点的 data 字段
- let isolationNodeData: any = {};
- if (isolationNodeDO.data) {
- try {
- isolationNodeData = typeof isolationNodeDO.data === 'string'
- ? JSON.parse(isolationNodeDO.data)
- : isolationNodeDO.data;
- } catch (e) {
- console.error('解析隔离节点data失败:', e);
- }
- }
-
- // 解析隔离点
- let isolationPoints: string[] = [];
- if (isolationNodeDO.isolationPoints) {
- try {
- isolationPoints = typeof isolationNodeDO.isolationPoints === 'string'
- ? JSON.parse(isolationNodeDO.isolationPoints)
- : (Array.isArray(isolationNodeDO.isolationPoints) ? isolationNodeDO.isolationPoints : []);
- } catch (e) {
- console.error('解析隔离点失败:', e);
- isolationPoints = isolationNodeData.isolationPoints || [];
- }
- } else {
- isolationPoints = isolationNodeData.isolationPoints || [];
- }
-
- // 从 nodeUserList 中提取上锁人和共锁人
- let lockPersonId: number | undefined = undefined;
- const coLockPersonIds: (number | string)[] = [];
- if (isolationNodeDO.nodeUserList && Array.isArray(isolationNodeDO.nodeUserList)) {
- isolationNodeDO.nodeUserList.forEach((user: any) => {
- if (user.type === 'jtlocker' && user.userId) {
- lockPersonId = typeof user.userId === 'number' ? user.userId : Number(user.userId);
- } else if (user.type === 'jtcolocker' && user.userId) {
- coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
- }
- });
- }
-
- // 复制隔离节点的所有数据到解除隔离节点
- return {
- ...prev,
- isolationNodeUuid: value || '',
- // 复制隔离方式
- isolationType: (isolationNodeDO.isolationType !== null && isolationNodeDO.isolationType !== undefined)
- ? String(isolationNodeDO.isolationType)
- : (isolationNodeData.isolationType || ''),
- // 复制隔离点
- isolationPoints: isolationPoints,
- // 复制负责人(如果隔离方式是盲板或拆除)
- responsible: isolationNodeDO.workerUserId
- ? (typeof isolationNodeDO.workerUserId === 'number' ? isolationNodeDO.workerUserId : Number(isolationNodeDO.workerUserId))
- : (isolationNodeData.workerUserId ? (typeof isolationNodeData.workerUserId === 'number' ? isolationNodeData.workerUserId : Number(isolationNodeData.workerUserId)) : undefined),
- // 复制上锁人
- lockPerson: lockPersonId || (isolationNodeData.lockPerson ? (typeof isolationNodeData.lockPerson === 'number' ? isolationNodeData.lockPerson : Number(isolationNodeData.lockPerson)) : undefined),
- // 复制共锁人
- coLockPersons: coLockPersonIds.length > 0
- ? coLockPersonIds
- : (isolationNodeData.coLockPersons && Array.isArray(isolationNodeData.coLockPersons)
- ? isolationNodeData.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id))
- : []),
- };
- }
-
- // 如果没找到 nodeDO,尝试从 workflowNodes 中获取
- const target = workflowNodes.find(n => n.id === value && n.data?.type === 'isolation');
- if (target) {
- const source = target.data || {};
- return {
- ...prev,
- isolationNodeUuid: value || '',
- isolationType: source.isolationType || '',
- isolationPoints: source.isolationPoints || [],
- isolationNode: Array.isArray(source.isolationNode)
- ? source.isolationNode
- : (source.isolationNode ? [source.isolationNode] : []),
- responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
- lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
- coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons)
- ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id))
- : [],
- };
- }
-
- return { ...prev, isolationNodeUuid: value || '' };
- });
- }}
- placeholder="请选择隔离节点"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- >
- {(() => {
- // 找到所有与当前"解除隔离"节点连线的"隔离/方案"节点
- // 连线方向:隔离节点(source) -> 解除隔离节点(target)
- const currentNodeId = selectedWorkflowNode?.id;
-
- // 找到所有以当前节点为 target 的 edge(隔离节点指向解除隔离节点)
- const incomingEdges = workflowEdges.filter(edge =>
- String(edge.target) === String(currentNodeId)
- );
-
- // 也检查反向连线(解除隔离节点指向隔离节点,虽然不常见但兼容)
- const outgoingEdges = workflowEdges.filter(edge =>
- String(edge.source) === String(currentNodeId)
- );
-
- // 收集所有已连线的隔离节点ID
- const connectedIsolationNodeIds = new Set<string>();
- incomingEdges.forEach(edge => {
- connectedIsolationNodeIds.add(String(edge.source));
- });
- outgoingEdges.forEach(edge => {
- connectedIsolationNodeIds.add(String(edge.target));
- });
-
- // 获取所有隔离节点
- const isolationNodes = workflowNodes.filter(n => n.data?.type === 'isolation');
-
- // 如果有连线,只显示已连线的隔离节点;否则显示所有隔离节点(兼容旧数据)
- const nodesToShow = connectedIsolationNodeIds.size > 0
- ? isolationNodes.filter(n => connectedIsolationNodeIds.has(String(n.id)))
- : isolationNodes;
-
- return nodesToShow.map(node => {
- // 从 workflowWorkNodeDOList 中查找对应的节点数据,获取 nodeName
- const nodeDO = workflowWorkNodeDOList.find(item => item.uuid === node.id && item.type === 'isolation');
- const displayName = nodeDO?.nodeName || node.data?.label || nodeConfigs.find(c => c.type === 'isolation')?.label || '隔离/方案';
-
- return (
- <Select.Option key={node.id} value={node.id}>
- {displayName}
- </Select.Option>
- );
- });
- })()}
- </Select>
- </div>
- )}
- {/* 隔离方式 */}
- <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={workflowNodeConfig.isolationType || undefined}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, isolationType: value || '' })
- }
- placeholder="请选择隔离方式"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
- >
- {isolationTypeDictList.map((item) => (
- <Select.Option key={item.id} value={item.value}>
- {item.label}
- </Select.Option>
- ))}
- </Select>
- </div>
- {/* 隔离点选择(可多选) */}
- {(selectedWorkflowNode.data?.type === 'isolation' || selectedWorkflowNode.data?.type === 'releaseIsolation') && (
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 隔离点选择(可多选) <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
- </label>
- <Select
- mode="multiple"
- value={workflowNodeConfig.isolationPoints}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, isolationPoints: value })
- }
- placeholder="请选择隔离点"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!min-h-10"
- allowClear
- disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
- >
- {workflowIsolationPoints.map((point: any, index) => (
- <Select.Option key={point.pointId || point.id || `point-${index}`} value={point.pointId || point.id}>{point.pointName}</Select.Option>
- ))}
- </Select>
- </div>
- )}
- {/* 盲板和拆除:显示负责人 */}
- {/* 字典值:0=盲板,1=上锁挂牌,2=拆除 */}
- {(workflowNodeConfig.isolationType === '0' || workflowNodeConfig.isolationType === '2') && (
- <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={workflowNodeConfig.responsible || undefined}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, responsible: value || undefined })
- }
- placeholder="请选择负责人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
- >
- {workflowDrawerUsers.map(user => (
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
- ))}
- </Select>
- </div>
- )}
- {/* 上锁挂牌:显示上锁人和共锁人 */}
- {workflowNodeConfig.isolationType === '1' && (
- <>
- <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={workflowNodeConfig.lockPerson || undefined}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, lockPerson: value || undefined })
- }
- placeholder="请选择上锁人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
- >
- {workflowLockerUsers.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={workflowNodeConfig.coLockPersons}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, coLockPersons: value })
- }
- placeholder="请选择共锁人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!min-h-10"
- allowClear
- disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
- >
- {workflowColockerUsers.map(user => (
- <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
- ))}
- </Select>
- </div>
- </>
- )}
- </>
- )}
- </div>
- </div>
- )}
- {/* 通知消息 - 创建作业节点不显示 */}
- {selectedWorkflowNode.data?.type !== 'createJob' && (
- <div>
- <h4 className="flex items-center gap-3 text-base font-semibold text-gray-900 mb-4">
- <span className="w-1 h-5 bg-blue-500 rounded-full flex-shrink-0" style={{ minWidth: '4px', minHeight: '20px' }}></span>
- 通知消息
- </h4>
- <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={workflowNodeConfig.notificationMethods?.sms || false}
- onChange={(e) =>
- setWorkflowNodeConfig({
- ...workflowNodeConfig,
- notificationMethods: {
- ...workflowNodeConfig.notificationMethods,
- sms: e.target.checked,
- },
- smsTemplateCode: e.target.checked ? 'true' : 'false',
- })
- }
- disabled={isViewMode}
- >
- 短信
- </Checkbox>
- </div>
- <div className="mb-3">
- <Checkbox
- checked={workflowNodeConfig.notificationMethods?.message || false}
- onChange={(e) =>
- setWorkflowNodeConfig({
- ...workflowNodeConfig,
- notificationMethods: {
- ...workflowNodeConfig.notificationMethods,
- message: e.target.checked,
- },
- messageTemplateCode: e.target.checked ? 'true' : 'false',
- })
- }
- disabled={isViewMode}
- >
- 站内信
- </Checkbox>
- </div>
- <div className="mb-3">
- <Checkbox
- checked={workflowNodeConfig.notificationMethods?.email || false}
- onChange={(e) =>
- setWorkflowNodeConfig({
- ...workflowNodeConfig,
- notificationMethods: {
- ...workflowNodeConfig.notificationMethods,
- email: e.target.checked,
- },
- emailTemplateCode: e.target.checked ? 'true' : 'false',
- })
- }
- disabled={isViewMode}
- >
- 邮件
- </Checkbox>
- </div>
- <div>
- <Checkbox
- checked={workflowNodeConfig.notificationMethods?.app || false}
- onChange={(e) =>
- setWorkflowNodeConfig({
- ...workflowNodeConfig,
- notificationMethods: {
- ...workflowNodeConfig.notificationMethods,
- app: e.target.checked,
- },
- appTemplateCode: e.target.checked ? 'true' : 'false',
- })
- }
- disabled={isViewMode}
- >
- APP通知
- </Checkbox>
- </div>
- </div>
- </div>
- {/* <div>
- <label className="block text-sm font-medium text-gray-700 mb-2">
- 通知人
- </label>
- <Select
- value={workflowNodeConfig.notificationPerson || undefined}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, notificationPerson: value || '' })
- }
- placeholder="请选择通知人"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={isViewMode}
- >
- <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={workflowNodeConfig.notificationTime || undefined}
- onChange={(value) =>
- setWorkflowNodeConfig({ ...workflowNodeConfig, notificationTime: value || '' })
- }
- placeholder="请选择通知时间"
- className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
- allowClear
- disabled={isViewMode}
- >
- <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>
- </div>
- )}
- </div>
-
- {/* 保存按钮 */}
- {!isViewMode && (
- <div className="px-4 py-3 border-t border-gray-200 bg-white sticky bottom-0 z-10">
- <Button
- type="primary"
- className="w-full"
- onClick={async () => {
- if (!selectedWorkflowNode) {
- message.warning(t('common.pleaseSelectNodeToConfig'));
- return;
- }
-
- // 验证当前节点配置
- const isValid = validateNodeConfig(workflowNodeConfig, selectedWorkflowNode.data?.type || '');
- if (!isValid) {
- message.warning(t('common.pleaseCompleteRequiredNodeConfig'));
- return;
- }
-
- try {
- // 查找对应的 workflowWorkNodeDO 节点(通过uuid匹配)
- const nodeDO = workflowWorkNodeDOList.find(item => item.uuid === selectedWorkflowNode.id);
-
- // 在外部作用域定义,确保在自动切换节点时可以使用
- let mergedWorkflowWorkNodeDOList: WorkflowWorkNodeDO[] = [];
-
- if (nodeDO && nodeDO.id && workflowWorkId) {
- // 构建节点用户信息列表
- const nodeUserDOList: WorkflowWorkNodeUserDO[] = [];
-
- // 如果是解除隔离节点,需要复制隔离节点的数据
- if (selectedWorkflowNode.data?.type === 'releaseIsolation' && workflowNodeConfig.isolationNodeUuid) {
- // 从 workflowWorkNodeDOList 中查找对应的隔离节点数据
- const isolationNodeDO = workflowWorkNodeDOList.find(item =>
- item.uuid === workflowNodeConfig.isolationNodeUuid && item.type === 'isolation'
- );
-
- if (isolationNodeDO) {
- // 复制隔离节点的 nodeUserList(上锁人、共锁人等)
- if (isolationNodeDO.nodeUserList && Array.isArray(isolationNodeDO.nodeUserList)) {
- isolationNodeDO.nodeUserList.forEach((user: any) => {
- if (user.type === 'jtlocker' || user.type === 'jtcolocker') {
- nodeUserDOList.push({
- userId: user.userId,
- type: user.type,
- });
- }
- });
- }
- }
- } else {
- // 如果不是解除隔离节点,使用原有逻辑
- // 如果隔离方式是"上锁挂牌"(字典值:1),添加上锁人员和共锁人员
- if (workflowNodeConfig.isolationType === '1') {
- // 添加上锁人员
- if (workflowNodeConfig.lockPerson) {
- nodeUserDOList.push({
- userId: Number(workflowNodeConfig.lockPerson),
- type: 'jtlocker',
- });
- }
-
- // 添加共锁人员(可以多选)
- if (workflowNodeConfig.coLockPersons && workflowNodeConfig.coLockPersons.length > 0) {
- workflowNodeConfig.coLockPersons.forEach((userId: string | number) => {
- nodeUserDOList.push({
- userId: Number(userId),
- type: 'jtcolocker',
- });
- });
- }
- }
- }
-
- // 调用更新节点接口
- const formId = workflowNodeConfig.submitForm ? Number(workflowNodeConfig.submitForm) : undefined;
-
- // 如果 formId 存在,先调用接口获取表单数据
- let formData: string | undefined = undefined;
- if (formId) {
- try {
- const formResponse = await getForm(formId);
- // 获取 data 字段的内容(如果接口返回的是 { code, data, message } 格式)
- let formDataObj: any = formResponse;
- if (formResponse && typeof formResponse === 'object' && 'data' in formResponse) {
- formDataObj = (formResponse as any).data || formResponse;
- }
- // 将整个对象内容转换成 JSON 字符串传递给 formData 字段
- formData = JSON.stringify(formDataObj);
- console.log('表单数据已转换为字符串:', formData);
- } catch (error) {
- console.error('获取表单数据失败:', error);
- // 即使获取失败,也继续保存节点,只是不传递 formData
- }
- }
-
- const updateParam: UpdateWorkflowWorkNodeParam = {
- nodeId: nodeDO.id,
- nodeName: workflowNodeConfig.nodeName,
- formId: formId,
- workerUserId: workflowNodeConfig.responsible ? Number(workflowNodeConfig.responsible) : undefined,
- isolationType: workflowNodeConfig.isolationType || undefined,
- isolationPoints: workflowNodeConfig.isolationPoints && workflowNodeConfig.isolationPoints.length > 0
- ? JSON.stringify(workflowNodeConfig.isolationPoints)
- : undefined,
- // 如果是解除隔离节点,传递选中的隔离节点UUID
- isolationNodeUuid: (selectedWorkflowNode.data?.type === 'releaseIsolation' && workflowNodeConfig.isolationNodeUuid)
- ? workflowNodeConfig.isolationNodeUuid
- : undefined,
- nodeUserDOList: nodeUserDOList.length > 0 ? nodeUserDOList : undefined,
- // 如果获取到了表单数据,将整个对象内容转换成字符串传递给 formData 字段
- formData: formData,
- // 根据选中值传递模板代码参数(字符串类型的 'true' 或 'false')
- smsTemplateCode: workflowNodeConfig.smsTemplateCode || 'false',
- messageTemplateCode: workflowNodeConfig.messageTemplateCode || 'false',
- emailTemplateCode: workflowNodeConfig.emailTemplateCode || 'false',
- appTemplateCode: workflowNodeConfig.appTemplateCode || 'false',
- };
-
- await workJobApi.updateWorkflowWorkNode(updateParam);
-
- // 更新成功后,立即调用详情接口获取最新数据
- let mergedWorkflowWorkNodeDOList: WorkflowWorkNodeDO[] = [];
- if (workflowWorkId) {
- try {
- const detailResponse = await workJobApi.selectWorkflowWorkById(workflowWorkId);
- const detail = detailResponse as any;
- if (detail.workflowWorkNodeDOList && Array.isArray(detail.workflowWorkNodeDOList)) {
- // 使用接口返回的最新数据更新 workflowWorkNodeDOList
- // 合并更新:保留现有数据,只更新接口返回的数据
- // 先计算合并后的数据,然后再更新状态
- const prev = workflowWorkNodeDOList;
- console.log('合并更新前的 workflowWorkNodeDOList:', prev);
- console.log('接口返回的 workflowWorkNodeDOList:', detail.workflowWorkNodeDOList);
-
- // 创建一个映射,方便查找
- const prevMap = new Map<string | number, WorkflowWorkNodeDO>();
- prev.forEach(item => {
- if (item.id) prevMap.set(item.id, item);
- if (item.uuid) prevMap.set(item.uuid, item);
- });
-
- const updatedList = detail.workflowWorkNodeDOList.map((newItem: WorkflowWorkNodeDO) => {
- // 查找现有列表中对应的节点(通过 id 或 uuid 匹配)
- const existingItem = (newItem.id && prevMap.get(newItem.id)) ||
- (newItem.uuid && prevMap.get(newItem.uuid)) ||
- null;
-
- // 如果找到现有节点,合并数据(优先使用接口返回的数据,但保留现有数据中接口没有返回的字段)
- if (existingItem) {
- // 合并数据,确保关键字段不被覆盖为空
- // 只有当接口返回的值是有效的(不是 null、undefined、0 或空字符串)时,才使用接口的值
- const merged: WorkflowWorkNodeDO = {
- ...existingItem,
- ...newItem,
- // 如果接口返回的 workerUserId 是 null、undefined 或 0,保留现有的值
- workerUserId: (newItem.workerUserId !== null && newItem.workerUserId !== undefined && newItem.workerUserId !== 0)
- ? newItem.workerUserId
- : existingItem.workerUserId,
- // 如果接口返回的 formId 是 null、undefined 或 0,保留现有的值
- formId: (newItem.formId !== null && newItem.formId !== undefined && newItem.formId !== 0)
- ? newItem.formId
- : existingItem.formId,
- // 如果接口返回的 isolationType 是 null 或 undefined,保留现有的值
- isolationType: (newItem.isolationType !== null && newItem.isolationType !== undefined && newItem.isolationType !== '')
- ? newItem.isolationType
- : existingItem.isolationType,
- // 如果接口返回的 isolationPoints 是 null 或 undefined,保留现有的值
- isolationPoints: (newItem.isolationPoints !== null && newItem.isolationPoints !== undefined && newItem.isolationPoints !== '')
- ? newItem.isolationPoints
- : existingItem.isolationPoints,
- // 如果接口返回的 nodeUserList 是空数组或不存在,保留现有的值
- nodeUserList: (newItem.nodeUserList && Array.isArray(newItem.nodeUserList) && newItem.nodeUserList.length > 0)
- ? newItem.nodeUserList
- : (existingItem.nodeUserList || []),
- };
-
- console.log(`合并节点 ${newItem.uuid || newItem.id}:`, {
- existing: { workerUserId: existingItem.workerUserId, formId: existingItem.formId },
- new: { workerUserId: newItem.workerUserId, formId: newItem.formId },
- merged: { workerUserId: merged.workerUserId, formId: merged.formId }
- });
-
- return merged;
- }
-
- // 如果没找到,直接使用接口返回的数据
- return newItem;
- });
-
- // 如果接口返回的列表中有新节点(不在现有列表中的),也要添加
- const existingUuids = new Set<string | number>();
- prev.forEach(item => {
- if (item.id) existingUuids.add(item.id);
- if (item.uuid) existingUuids.add(item.uuid);
- });
-
- const newItems = detail.workflowWorkNodeDOList.filter((item: WorkflowWorkNodeDO) => {
- const itemKey = item.id || item.uuid;
- return itemKey && !existingUuids.has(itemKey);
- });
-
- mergedWorkflowWorkNodeDOList = [...updatedList, ...newItems];
- console.log('合并更新后的 workflowWorkNodeDOList:', mergedWorkflowWorkNodeDOList);
-
- // 更新状态
- setWorkflowWorkNodeDOList(mergedWorkflowWorkNodeDOList);
- }
- } catch (error: any) {
- console.error('获取最新节点数据失败:', error);
- // 如果接口调用失败,仍然使用本地更新逻辑作为后备
- setWorkflowWorkNodeDOList(prev => prev.map(item => {
- if (item.id === nodeDO.id) {
- return {
- ...item,
- nodeName: workflowNodeConfig.nodeName,
- formId: workflowNodeConfig.submitForm ? Number(workflowNodeConfig.submitForm) : undefined,
- workerUserId: workflowNodeConfig.responsible ? Number(workflowNodeConfig.responsible) : undefined,
- isolationType: workflowNodeConfig.isolationType || undefined,
- isolationPoints: workflowNodeConfig.isolationPoints && workflowNodeConfig.isolationPoints.length > 0
- ? JSON.stringify(workflowNodeConfig.isolationPoints)
- : undefined,
- lockPerson: workflowNodeConfig.lockPerson ? String(workflowNodeConfig.lockPerson) : undefined,
- notifyTime: workflowNodeConfig.notificationTime || undefined,
- data: JSON.stringify((() => {
- const existingData = item.data ? (typeof item.data === 'string' ? JSON.parse(item.data) : item.data) : {};
- const { isolationMethod, ...restExistingData } = existingData;
- return {
- ...restExistingData,
- label: workflowNodeConfig.nodeName,
- icon: workflowNodeConfig.nodeIcon,
- responsible: workflowNodeConfig.responsible ? String(workflowNodeConfig.responsible) : '',
- remark: workflowNodeConfig.remark,
- submitForm: workflowNodeConfig.submitForm ? String(workflowNodeConfig.submitForm) : '',
- isolationType: workflowNodeConfig.isolationType,
- isolationPoints: workflowNodeConfig.isolationPoints,
- isolationNode: workflowNodeConfig.isolationNode,
- isolationNodeUuid: workflowNodeConfig.isolationNodeUuid,
- lockPerson: workflowNodeConfig.lockPerson ? String(workflowNodeConfig.lockPerson) : '',
- coLockPersons: workflowNodeConfig.coLockPersons.map((id: any) => String(id)),
- notificationMethods: workflowNodeConfig.notificationMethods,
- notificationPerson: workflowNodeConfig.notificationPerson,
- notificationTime: workflowNodeConfig.notificationTime,
- };
- })()),
- };
- }
- return item;
- }));
- }
- }
- }
-
- // 缓存当前节点配置
- const newCache = new Map(workflowNodeConfigCache);
- newCache.set(selectedWorkflowNode.id, { ...workflowNodeConfig });
- setWorkflowNodeConfigCache(newCache);
-
- // 标记当前节点为已完成
- const newCompleted = new Set(completedNodeIds);
- newCompleted.add(selectedWorkflowNode.id);
- setCompletedNodeIds(newCompleted);
-
- // 更新节点保存状态缓存
- const newSavedStatusCache = new Map(nodeSavedStatusCache);
- newSavedStatusCache.set(selectedWorkflowNode.id, true);
- setNodeSavedStatusCache(newSavedStatusCache);
-
- // 更新节点显示
- const { isolationMethod, ...restData } = selectedWorkflowNode.data || {};
- const updatedData = {
- ...restData,
- label: workflowNodeConfig.nodeName,
- icon: workflowNodeConfig.nodeIcon,
- responsible: workflowNodeConfig.responsible,
- remark: workflowNodeConfig.remark,
- submitForm: workflowNodeConfig.submitForm,
- isolationType: workflowNodeConfig.isolationType,
- isolationPoints: workflowNodeConfig.isolationPoints,
- isolationNode: workflowNodeConfig.isolationNode,
- isolationNodeUuid: workflowNodeConfig.isolationNodeUuid,
- lockPerson: workflowNodeConfig.lockPerson,
- coLockPersons: workflowNodeConfig.coLockPersons,
- notificationMethods: workflowNodeConfig.notificationMethods,
- notificationPerson: workflowNodeConfig.notificationPerson,
- notificationTime: workflowNodeConfig.notificationTime,
- completed: true, // 标记为已完成
- };
- setWorkflowNodes((nds) =>
- nds.map((node) =>
- node.id === selectedWorkflowNode.id
- ? { ...node, data: updatedData, selected: false }
- : node
- )
- );
-
- // 查找下一个未完成的节点(使用新的completedSet,而不是状态中的)
- // 优先查找当前节点的子节点
- const nextNode = getNextIncompleteNode(newCompleted, selectedWorkflowNode.id);
- if (nextNode) {
- // 自动切换到下一个节点
- // 使用合并后的最新数据(如果有的话),否则使用状态中的数据
- const latestWorkflowWorkNodeDOList = mergedWorkflowWorkNodeDOList.length > 0
- ? mergedWorkflowWorkNodeDOList
- : workflowWorkNodeDOList;
-
- setTimeout(() => {
- setSelectedWorkflowNode(nextNode);
- const cachedConfig = newCache.get(nextNode.id);
- if (cachedConfig) {
- setWorkflowNodeConfig(cachedConfig);
- } else {
- // 从最新的 workflowWorkNodeDOList 中查找节点数据
- const nextNodeDO = latestWorkflowWorkNodeDOList.find(item => item.uuid === nextNode.id);
- if (nextNodeDO) {
- let nextNodeData: any = {};
- if (nextNodeDO.data) {
- try {
- nextNodeData = typeof nextNodeDO.data === 'string' ? JSON.parse(nextNodeDO.data) : nextNodeDO.data;
- } catch (e) {
- console.error('解析节点data失败:', e);
- }
- }
- const source = nextNodeData || nextNode.data || {};
- const config = nodeConfigs.find(c => c.type === (nextNodeDO.type || source.type));
- setWorkflowNodeConfig({
- nodeName: nextNodeDO.nodeName || source.label || config?.label || '',
- nodeIcon: nextNodeDO.nodeIcon || source.icon || '',
- responsible: (nextNodeDO.workerUserId !== null && nextNodeDO.workerUserId !== undefined && nextNodeDO.workerUserId !== 0)
- ? (typeof nextNodeDO.workerUserId === 'number' ? nextNodeDO.workerUserId : Number(nextNodeDO.workerUserId))
- : (source.workerUserId && source.workerUserId !== '' && source.workerUserId !== '0')
- ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId))
- : undefined,
- remark: source.remark || '',
- submitForm: nextNodeDO.formId ? (typeof nextNodeDO.formId === 'number' ? nextNodeDO.formId : Number(nextNodeDO.formId)) : (source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined),
- isolationType: (nextNodeDO.isolationType !== null && nextNodeDO.isolationType !== undefined) ? String(nextNodeDO.isolationType) : (source.isolationType || ''),
- isolationPoints: nextNodeDO.isolationPoints ? (typeof nextNodeDO.isolationPoints === 'string' ? JSON.parse(nextNodeDO.isolationPoints) : nextNodeDO.isolationPoints) : (source.isolationPoints || []),
- isolationNode: source.isolationNode || [],
- isolationNodeUuid: nextNodeDO.isolationNodeUuid || source.isolationNodeUuid || '',
- lockPerson: (() => {
- // 优先从 nodeUserList 中提取
- let lockPersonId: number | undefined = undefined;
- if (nextNodeDO.nodeUserList && Array.isArray(nextNodeDO.nodeUserList)) {
- const lockerUser = nextNodeDO.nodeUserList.find((user: any) => user.type === 'jtlocker');
- if (lockerUser?.userId) {
- lockPersonId = typeof lockerUser.userId === 'number' ? lockerUser.userId : Number(lockerUser.userId);
- }
- }
- return lockPersonId || (source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined);
- })(),
- coLockPersons: (() => {
- // 优先从 nodeUserList 中提取
- const coLockPersonIds: number[] = [];
- if (nextNodeDO.nodeUserList && Array.isArray(nextNodeDO.nodeUserList)) {
- nextNodeDO.nodeUserList.forEach((user: any) => {
- if (user.type === 'jtcolocker' && user.userId) {
- coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
- }
- });
- }
- return coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
- })(),
- notificationMethods: source.notificationMethods || {
- sms: false,
- message: false,
- email: false,
- app: false,
- },
- notificationPerson: source.notificationPerson || '',
- notificationTime: nextNodeDO.notifyTime || source.notificationTime || '',
- });
- } else {
- const source = nextNode.data || {};
- const config = nodeConfigs.find(c => c.type === source.type);
- setWorkflowNodeConfig({
- nodeName: source.label || config?.label || '',
- nodeIcon: source.icon || '',
- responsible: source.responsible ? (typeof source.responsible === 'number' ? source.responsible : Number(source.responsible)) : undefined,
- remark: source.remark || '',
- submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
- isolationType: source.isolationType || '',
- isolationPoints: source.isolationPoints || [],
- 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 || '',
- });
- }
- }
- // 高亮下一个节点
- setWorkflowNodes((nds) =>
- nds.map((node) =>
- node.id === nextNode.id
- ? { ...node, selected: true }
- : { ...node, selected: false }
- )
- );
- // 不自动聚焦,保持当前视图,让用户能看到其他节点
- // if (workflowReactFlowInstance) {
- // workflowReactFlowInstance.fitView({ padding: 0.2, duration: 300, nodes: [nextNode] });
- // }
- }, 100);
- message.success(t('common.nodeConfigSaved'));
- } else {
- // 所有节点都已完成,调用检查接口
- if (workflowWorkId) {
- try {
- const checkResponse = await workJobApi.checkWorkById(workflowWorkId);
- const checkData = (checkResponse as any)?.data || checkResponse;
- const errorMessages = Array.isArray(checkData) ? checkData : (checkData ? [checkData] : []);
-
- // 如果返回的错误信息数组为空或null,表示所有节点配置完毕
- if (!errorMessages || errorMessages.length === 0) {
- message.success(t('common.allNodeConfigCompleted'));
- } else {
- // 有错误信息,显示弹框
- Modal.warning({
- title: t('common.nodeConfigIncomplete'),
- width: 500,
- content: (
- <div style={{ marginTop: 16 }}>
- <p style={{ marginBottom: 12, color: '#666' }}>{t('common.nodeConfigIncompleteText')}</p>
- <ul style={{ margin: 0, paddingLeft: 20 }}>
- {errorMessages.map((msg: string, index: number) => (
- <li key={index} style={{ marginBottom: 8, color: '#ff4d4f' }}>
- {msg}
- </li>
- ))}
- </ul>
- </div>
- ),
- okText: t('common.iKnow'),
- });
- }
- } catch (error: any) {
- console.error('检查作业失败:', error);
- message.error(error?.message || t('common.checkWorkFailed'));
- }
- } else {
- message.success(t('common.allNodeConfigCompleted'));
- }
- }
- } catch (error: any) {
- console.error('保存节点配置失败:', error);
- message.error(error?.message || t('common.saveFailed'));
- }
- }}
- >
- 保存当前节点
- </Button>
- </div>
- )}
- </div>
- ) : (
- <div className="p-4">
- <div className="text-sm text-gray-600">
- 请选择左侧流程中的节点进行配置
- </div>
- </div>
- )}
- </div>
- </div>
- )}
- {workJobStep === 2 && (
- <Form
- form={workJobPublishForm}
- layout="vertical"
- className="max-w-2xl"
- >
- <div className="mb-4">
- <h3 className="text-base font-medium text-gray-900 mb-2">发布作业设置</h3>
- <p className="text-sm text-gray-500">请配置作业的执行时间并最终发布</p>
- </div>
- <Form.Item
- label="发布方式"
- name="startType"
- required
- rules={[{ required: true, message: '请选择发布方式' }]}
- initialValue="0"
- >
- <Radio.Group disabled={isViewMode}>
- <Radio value="0">立即发布</Radio>
- <Radio value="1">定时发布</Radio>
- </Radio.Group>
- </Form.Item>
- <Form.Item
- noStyle
- shouldUpdate={(prevValues, currentValues) =>
- prevValues.startType !== currentValues.startType
- }
- >
- {({ getFieldValue }) =>
- getFieldValue('startType') === '1' ? (
- <Form.Item
- label="发布时间"
- name="planTime"
- required
- rules={[{ required: true, message: '请选择发布时间' }]}
- >
- <DatePicker
- showTime
- format="YYYY-MM-DD HH:mm:ss"
- className="w-full"
- placeholder="请选择发布时间"
- disabled={isViewMode}
- />
- </Form.Item>
- ) : null
- }
- </Form.Item>
- </Form>
- )}
- </div>
- {/* 底部按钮 */}
- <div className="px-6 py-4 border-t border-gray-200 flex justify-center gap-2">
- {workJobStep === 0 && (
- <>
- <Button onClick={() => {
- setShowAddModal(false);
- setIsViewMode(false);
- setWorkJobStep(0);
- workJobBasicForm.resetFields();
- workJobPublishForm.resetFields();
- setWorkflowJson(null);
- setWorkflowWorkId(null); // 清空作业ID
- // 清空流程管理相关状态
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setSelectedWorkflowNode(null);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- setNodeSavedStatusCache(new Map());
- setWorkflowWorkNodeDOList([]); // 清空节点数据列表
- setOriginalBasicFormData(null); // 清空原始表单数据
- // 关闭弹框时刷新列表
- getWorkJobList();
- }}>
- {isViewMode ? '关闭' : '取消'}
- </Button>
- {!isViewMode ? (
- <Button
- type="primary"
- onClick={async () => {
- try {
- // 验证必填项
- const values = await workJobBasicForm.validateFields();
-
- // 判断是新增还是更新
- let currentWorkId: number | null = null;
- if (workflowWorkId) {
- // 已有ID,判断是否有修改
- // 如果 originalBasicFormData 为 null,说明是新增后第一次返回,需要从服务器获取最新数据进行比较
- let hasChanges = false;
- if (originalBasicFormData) {
- // 有原始数据,直接比较
- hasChanges = (
- originalBasicFormData.workflowTemplate !== values.workflowTemplate ||
- originalBasicFormData.jobCategory !== values.jobCategory ||
- originalBasicFormData.jobName !== values.jobName ||
- originalBasicFormData.jobContent !== values.jobContent ||
- originalBasicFormData.urgency !== values.urgency
- );
- } else {
- // 没有原始数据,从服务器获取最新数据进行比较
- try {
- const detailResponse = await workJobApi.selectWorkflowWorkById(workflowWorkId);
- const detail = detailResponse as any;
- const currentFormData = {
- workflowTemplate: values.workflowTemplate,
- jobCategory: values.jobCategory,
- jobName: values.jobName,
- jobContent: values.jobContent,
- urgency: values.urgency,
- };
- const serverData = {
- workflowTemplate: detail.designId || detail.workflowTemplate,
- jobCategory: detail.type || detail.jobCategory,
- jobName: detail.name || detail.jobName,
- jobContent: detail.description || detail.jobContent,
- urgency: detail.urgencyLevel || detail.urgency,
- };
- hasChanges = (
- serverData.workflowTemplate !== currentFormData.workflowTemplate ||
- serverData.jobCategory !== currentFormData.jobCategory ||
- serverData.jobName !== currentFormData.jobName ||
- serverData.jobContent !== currentFormData.jobContent ||
- serverData.urgency !== currentFormData.urgency
- );
- // 设置原始数据为服务器数据
- setOriginalBasicFormData(serverData);
- } catch (error: any) {
- console.error('获取作业详情失败:', error);
- // 如果获取失败,假设有变化,调用更新接口
- hasChanges = true;
- }
- }
-
- if (hasChanges) {
- // 有修改,调用更新接口
- await workJobApi.updateWorkflowWork({
- id: workflowWorkId, // 作业票ID
- name: values.jobName, // 作业名称
- type: values.jobCategory, // 作业分类
- designId: values.workflowTemplate, // 流程设计ID
- description: values.jobContent, // 作业内容
- urgencyLevel: values.urgency, // 紧急程度
- });
- message.success(t('common.basicInfoUpdateSuccess'));
- // 更新原始数据
- setOriginalBasicFormData({
- workflowTemplate: values.workflowTemplate,
- jobCategory: values.jobCategory,
- jobName: values.jobName,
- jobContent: values.jobContent,
- urgency: values.urgency,
- });
- }
- // 无论是否有修改,都使用当前的 workflowWorkId
- currentWorkId = workflowWorkId;
- } else {
- // 没有ID,调用新增接口
- const response = await workJobApi.insertWorkflowWork({
- name: values.jobName, // 作业名称
- type: values.jobCategory, // 作业分类
- designId: values.workflowTemplate, // 流程设计ID
- description: values.jobContent, // 作业内容
- urgencyLevel: values.urgency, // 紧急程度
- });
-
- // 保存返回的作业ID
- // 响应格式: { code: 0, data: 13, msg: "" }
- // axios拦截器可能已经处理了数据格式,直接使用response
- const responseData = response as any;
- // 尝试多种方式获取ID
- const workId = responseData?.data || responseData?.id || (typeof responseData === 'number' ? responseData : null);
- if (workId) {
- currentWorkId = Number(workId);
- setWorkflowWorkId(currentWorkId);
- console.log('保存作业ID:', workId);
- }
-
- // 新增成功后,设置原始表单数据,用于后续判断是否有修改
- setOriginalBasicFormData({
- workflowTemplate: values.workflowTemplate,
- jobCategory: values.jobCategory,
- jobName: values.jobName,
- jobContent: values.jobContent,
- urgency: values.urgency,
- });
-
- message.success(t('common.basicInfoSaveSuccess'));
- }
-
- // 成功后进入下一步
- setWorkJobStep(1);
-
- // 注意:节点状态初始化会在 useEffect 中自动执行(当 workJobStep === 1 且 workflowWorkId 存在时)
- // 这里不需要手动加载,让 useEffect 统一处理
- } catch (error: any) {
- if (error.errorFields) {
- // 表单验证失败
- message.error(t('common.pleaseCompleteRequiredFields'));
- } else {
- // API调用失败
- message.error(error?.message || t('common.saveFailed'));
- }
- }
- }}
- >
- 下一步
- </Button>
- ) : (
- <Button
- type="primary"
- onClick={() => setWorkJobStep(1)}
- >
- 下一步
- </Button>
- )}
- </>
- )}
- {workJobStep === 1 && (
- <div className="flex flex-col items-center w-full gap-2">
- <div className="flex items-center gap-2">
- <Button onClick={() => setWorkJobStep(0)}>
- 上一步
- </Button>
- {!isViewMode && (
- <>
- <div className="flex items-center gap-2">
- <Button
- type="primary"
- disabled={hasUnsavedNodes}
- className={!hasUnsavedNodes ? 'next-step-button-focus' : ''}
- onClick={async () => {
- // 调用检查接口,检查是否有未保存的节点
- if (!workflowWorkId) {
- message.warning(t('common.workIdNotExistsForCheck'));
- return;
- }
-
- try {
- const checkResponse = await workJobApi.checkWorkById(workflowWorkId);
- const checkData = (checkResponse as any)?.data || checkResponse;
- const errorMessages = Array.isArray(checkData) ? checkData : (checkData ? [checkData] : []);
-
- // 如果返回的错误信息数组为空或null,表示所有节点配置完毕,可以进入下一个tab
- if (!errorMessages || errorMessages.length === 0) {
- message.success(t('common.allNodeConfigCompleted'));
- // 进入下一个tab
- setWorkJobStep(2);
- } else {
- // 有错误信息,显示弹框,不能进入下一个tab
- Modal.warning({
- title: t('common.nodeConfigIncomplete'),
- width: 500,
- content: (
- <div style={{ marginTop: 16 }}>
- <p style={{ marginBottom: 12, color: '#666' }}>{t('common.nodeConfigIncompleteText')}</p>
- <ul style={{ margin: 0, paddingLeft: 20 }}>
- {errorMessages.map((msg: string, index: number) => (
- <li key={index} style={{ marginBottom: 8, color: '#ff4d4f' }}>
- {msg}
- </li>
- ))}
- </ul>
- </div>
- ),
- okText: t('common.iKnow'),
- });
- }
- } catch (error: any) {
- console.error('检查作业失败:', error);
- message.error(error?.message || t('common.checkWorkFailed'));
- }
- }}
- >
- 下一步
- </Button>
- {!hasUnsavedNodes && (
- <img
- src={new URL('../assets/finger.png', import.meta.url).href}
- alt="指向下一步"
- className="pointer-hint-icon"
- style={{
- width: '32px',
- height: '32px',
- animation: 'pointing 1.5s ease-in-out infinite',
- display: 'inline-block',
- cursor: 'pointer',
- marginLeft: '8px',
- objectFit: 'contain'
- }}
- />
- )}
- <style>{`
- @keyframes focusPulse {
- 0%, 100% {
- box-shadow: 0 0 0 0 rgba(22, 119, 255, 0.7);
- transform: scale(1);
- }
- 50% {
- box-shadow: 0 0 0 10px rgba(22, 119, 255, 0);
- transform: scale(1.05);
- }
- }
- .next-step-button-focus {
- animation: focusPulse 2s ease-in-out infinite;
- }
- @keyframes pointing {
- 0%, 100% {
- transform: translateX(0);
- }
- 25% {
- transform: translateX(-4px);
- }
- 50% {
- transform: translateX(0);
- }
- 75% {
- transform: translateX(4px);
- }
- }
- .pointer-hint-icon {
- cursor: pointer;
- }
- `}</style>
- </div>
- {/* <Button
- type="default"
- onClick={async () => {
- if (!workflowWorkId) {
- message.warning(t('common.workIdNotExistsForCheck'));
- return;
- }
-
- try {
- const checkResponse = await workJobApi.checkWorkById(workflowWorkId);
- const checkData = (checkResponse as any)?.data || checkResponse;
- const errorMessages = Array.isArray(checkData) ? checkData : (checkData ? [checkData] : []);
-
- // 如果返回的错误信息数组为空或null,表示所有节点配置完毕,可以跳转
- if (!errorMessages || errorMessages.length === 0) {
- message.success(t('common.allNodeConfigCompleted'));
- // 延迟一下再跳转,让用户看到成功提示
- setTimeout(() => {
- setWorkJobStep(2);
- }, 500);
- } else {
- // 有错误信息,显示弹框,不跳转
- Modal.warning({
- title: t('common.nodeConfigIncomplete'),
- width: 500,
- content: (
- <div style={{ marginTop: 16 }}>
- <p style={{ marginBottom: 12, color: '#666' }}>{t('common.nodeConfigIncompleteText')}</p>
- <ul style={{ margin: 0, paddingLeft: 20 }}>
- {errorMessages.map((msg: string, index: number) => (
- <li key={index} style={{ marginBottom: 8, color: '#ff4d4f' }}>
- {msg}
- </li>
- ))}
- </ul>
- </div>
- ),
- okText: t('common.iKnow'),
- });
- }
- } catch (error: any) {
- console.error('检查作业失败:', error);
- message.error(error?.message || t('common.checkWorkFailed'));
- }
- }}
- >
- 一键保存
- </Button> */}
- </>
- )}
- {isViewMode && (
- <Button
- type="primary"
- onClick={() => setWorkJobStep(2)}
- >
- 下一步
- </Button>
- )}
- </div>
- <div className="flex items-center text-red-600" style={{ fontSize: '12px' }}>
- <span className="mr-1">⚠️</span>
- <span>提示:流程中的每一个节点都需要单独配置并保存,方可点击 "下一步"。</span>
- </div>
- </div>
- )}
- {workJobStep === 2 && (
- <>
- <Button onClick={() => setWorkJobStep(1)}>
- 上一步
- </Button>
- {!isViewMode ? (
- <Button
- type="primary"
- icon={<Play className="w-4 h-4" />}
- onClick={async () => {
- try {
- const publishValues = await workJobPublishForm.validateFields();
-
- if (!workflowWorkId) {
- message.error(t('common.workIdNotExists'));
- return;
- }
-
- // 构建发布参数
- const updateStartWorkParam: UpdateStartWorkParam = {
- workId: Number(workflowWorkId), // 确保是 number 类型
- startType: publishValues.startType || '0', // 0-立即发布 1-定时发布
- };
-
- // 如果是定时发布,需要传递发布时间
- if (publishValues.startType === '1') {
- if (!publishValues.planTime) {
- message.error(t('common.scheduledPublishTimeRequired'));
- return;
- }
-
- // 将 DatePicker 返回的 dayjs 对象转换为字符串格式 (YYYY-MM-DD HH:mm:ss)
- const planTime = publishValues.planTime;
- if (planTime && typeof planTime.format === 'function') {
- // dayjs 对象
- updateStartWorkParam.planTime = planTime.format('YYYY-MM-DD HH:mm:ss');
- } else if (planTime instanceof Date) {
- // Date 对象
- const year = planTime.getFullYear();
- const month = String(planTime.getMonth() + 1).padStart(2, '0');
- const day = String(planTime.getDate()).padStart(2, '0');
- const hours = String(planTime.getHours()).padStart(2, '0');
- const minutes = String(planTime.getMinutes()).padStart(2, '0');
- const seconds = String(planTime.getSeconds()).padStart(2, '0');
- updateStartWorkParam.planTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
- } else if (typeof planTime === 'string') {
- // 字符串格式
- updateStartWorkParam.planTime = planTime;
- } else {
- message.error(t('common.publishTimeFormatError'));
- return;
- }
- }
-
- console.log('发布作业参数:', updateStartWorkParam);
-
- // 调用发布作业接口
- await workJobApi.updateStartWork(updateStartWorkParam);
-
- message.success(t('common.publishWorkSuccess'));
- setShowAddModal(false);
- setWorkJobStep(0);
- workJobBasicForm.resetFields();
- workJobPublishForm.resetFields();
- setWorkflowJson(null);
- setWorkflowWorkId(null); // 清空作业ID
- // 清空流程管理相关状态
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setSelectedWorkflowNode(null);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- // 清除节点保存状态缓存
- setNodeSavedStatusCache(new Map());
- // 关闭弹框时刷新列表
- getWorkJobList();
- } catch (error: any) {
- if (error.errorFields) {
- return;
- }
- console.error('发布作业失败:', error);
- message.error(error?.message || t('common.publishWorkFailed'));
- }
- }}
- >
- 确认发布
- </Button>
- ) : (
- <Button onClick={() => {
- setShowAddModal(false);
- setIsViewMode(false);
- setWorkJobStep(0);
- workJobBasicForm.resetFields();
- workJobPublishForm.resetFields();
- setWorkflowJson(null);
- setWorkflowWorkId(null);
- setWorkflowNodes([]);
- setWorkflowEdges([]);
- setSelectedWorkflowNode(null);
- setWorkflowNodeConfigCache(new Map());
- setCompletedNodeIds(new Set());
- setNodeSavedStatusCache(new Map());
- setWorkflowWorkNodeDOList([]);
- setOriginalBasicFormData(null);
- getWorkJobList();
- }}>
- 关闭
- </Button>
- )}
- </>
- )}
- </div>
- </div>
- </Modal>
- ) : null}
- {/* 编辑作业弹窗(保持原有逻辑) */}
- {subMenu === '作业管理' && showAddModal && editingItem ? (
- <Modal
- title={t('common.editWork')}
- open={showAddModal}
- onCancel={() => {
- setShowAddModal(false);
- setEditingItem(null);
- form.resetFields();
- }}
- footer={[
- <Button key="cancel" onClick={() => {
- setShowAddModal(false);
- setEditingItem(null);
- form.resetFields();
- }}>
- {t('common.cancel')}
- </Button>,
- <Button key="submit" type="primary" onClick={async () => {
- try {
- const values = await form.validateFields();
- await workJobApi.updateWorkflowWork({
- id: editingItem.id!,
- name: values.name,
- type: values.type || editingItem.type || '',
- designId: values.designId || editingItem.designId || editingItem.workflowDesignId || 0,
- description: values.content || '',
- urgencyLevel: values.urgencyLevel || editingItem.urgencyLevel || '',
- });
- message.success(t('common.updateWorkSuccess'));
- setShowAddModal(false);
- setEditingItem(null);
- form.resetFields();
- getWorkJobList();
- } catch (error: any) {
- if (error.errorFields) {
- return;
- }
- console.error('更新作业失败:', error);
- message.error(error?.message || t('common.updateWorkFailed'));
- }
- }}>
- {t('common.confirm')}
- </Button>
- ]}
- width={600}
- destroyOnClose
- styles={{
- footer: {
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- padding: '16px 24px'
- }
- }}
- >
- <Form
- form={form}
- labelCol={{ span: 4 }}
- wrapperCol={{ span: 20 }}
- key={editingItem?.id || 'new'}
- >
- <Form.Item
- label={t('common.workName')}
- name="name"
- rules={[{ required: true, message: t('common.workNameRequired') }]}
- >
- <Input placeholder={t('common.workNamePlaceholder')} />
- </Form.Item>
- <Form.Item
- label={t('common.workContent')}
- name="content"
- >
- <Input.TextArea rows={4} placeholder={t('common.workContentPlaceholder')} />
- </Form.Item>
- </Form>
- </Modal>
- ) : null}
- </div>
- );
- }
- return (
- <div className="space-y-6">
- {/* 表格容器 - 合并工具栏和列表 */}
- <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
- {/* 工具栏 */}
- <div className="p-4 border-b border-gray-200/50">
- <div className="flex items-center justify-between">
- <div className="relative w-80">
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
- <input
- type="text"
- placeholder={subMenu === '流程模板' ? t('form.searchTemplate') : `搜索${subMenu}...`}
- value={searchTerm}
- onChange={(e) => setSearchTerm(e.target.value)}
- className="w-full h-10 pl-10 pr-4 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:border-blue-400 focus:ring-4 focus:ring-blue-100 transition-all text-sm"
- />
- </div>
- <button
- onClick={() => {
- setEditingItem(null);
- setShowAddModal(true);
- }}
- className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
- >
- <Plus className="w-4 h-4" strokeWidth={2.5} />
- <span className="text-sm">
- {subMenu === '流程模板' && t('common.addTemplate')}
- {subMenu === 'SOP管理' && '新增SOP'}
- {subMenu === '作业管理' && t('common.addWork')}
- </span>
- </button>
- </div>
- </div>
- {/* 表格 */}
- <div className="overflow-x-auto">
- <table className="w-full">
- <thead>
- <tr className="bg-gradient-to-r from-gray-50 to-gray-100/50 border-b border-gray-200">
- <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
- {t('common.serialNumber')}
- </th>
- {columns.map((column) => (
- <th
- key={column.key}
- className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider"
- style={{ width: column.width }}
- >
- {column.label}
- </th>
- ))}
- <th className="px-6 py-4 text-center text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
- {t('table.operation')}
- </th>
- </tr>
- </thead>
- <tbody className="divide-y divide-gray-100">
- {filteredData.map((row, index) => (
- <tr
- key={row.id}
- className="hover:bg-blue-50/30 transition-colors"
- >
- <td className="px-6 py-4 text-sm text-gray-900">
- {index + 1}
- </td>
- {columns.map((column) => (
- <td key={column.key} className="px-6 py-4 text-sm text-gray-900">
- {column.key === 'status' ? (
- <span
- className={`inline-flex px-3 py-1 rounded-lg text-xs ${
- row[column.key] === '启用' || row[column.key] === '有效' || row[column.key] === '已完成'
- ? 'bg-green-100 text-green-700'
- : row[column.key] === '待审批' || row[column.key] === '待审核'
- ? 'bg-orange-100 text-orange-700'
- : row[column.key] === '进行中'
- ? 'bg-blue-100 text-blue-700'
- : 'bg-gray-100 text-gray-700'
- }`}
- >
- {row[column.key]}
- </span>
- ) : column.key === 'priority' ? (
- <span
- className={`inline-flex px-3 py-1 rounded-lg text-xs ${
- row[column.key] === '紧急'
- ? 'bg-red-100 text-red-700'
- : row[column.key] === '重要'
- ? 'bg-orange-100 text-orange-700'
- : 'bg-blue-100 text-blue-700'
- }`}
- >
- {row[column.key]}
- </span>
- ) : (
- row[column.key]
- )}
- </td>
- ))}
- <td className="px-6 py-4">
- <div className="flex items-center justify-center gap-2">
- <Button
- variant="ghost"
- size="sm"
- className="h-8 px-2"
- >
- <Eye className="w-4 h-4" />
- <span className="ml-1">{t('table.view')}</span>
- </Button>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleEdit(row)}
- className="h-8 px-2"
- >
- <Edit2 className="w-4 h-4" />
- <span className="ml-1">{t('common.edit')}</span>
- </Button>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleDelete(row.id)}
- className="h-8 px-2 text-red-600 hover:text-red-700"
- >
- <Trash2 className="w-4 h-4" />
- <span className="ml-1">{t('common.delete')}</span>
- </Button>
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- {/* 分页 */}
- {filteredData.length > 0 && (
- <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
- <div className="flex items-center justify-between">
- <div className="text-sm text-gray-600">
- {t('common.total')} <span className="text-blue-600 font-medium">{filteredData.length}</span> {t('common.records')}
- </div>
- <div className="flex gap-2">
- <Button
- disabled={true}
- >
- {t('common.prevPage')}
- </Button>
- <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
- 1 / 1
- </span>
- <Button
- disabled={true}
- >
- {t('common.nextPage')}
- </Button>
- </div>
- </div>
- </div>
- )}
- {/* 新增/编辑弹窗 */}
- {showAddModal && (
- <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
- <div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto animate-in zoom-in duration-200">
- {/* 弹窗标题 */}
- <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between sticky top-0 bg-white z-10">
- <h3 className="text-lg text-gray-900">
- {editingItem
- ? (subMenu === '流程模板' ? t('common.editTemplate') : subMenu === 'SOP管理' ? '编辑SOP' : subMenu === '作业管理' ? t('common.editWork') : subMenu === '表单管理' ? t('common.editFormManagement') : subMenu === '流程设计' ? t('common.editProcessDesign') : t('common.edit'))
- : (subMenu === '流程模板' ? t('common.addTemplateTitle') : subMenu === 'SOP管理' ? '新增SOP' : subMenu === '作业管理' ? t('common.addWork') : subMenu === '表单管理' ? t('common.addFormManagement') : subMenu === '流程设计' ? t('common.addProcessDesign') : t('common.addNew'))
- }
- </h3>
- <button
- onClick={() => setShowAddModal(false)}
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
- >
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
- </svg>
- </button>
- </div>
- {/* 弹窗内容 */}
- <div className="px-6 py-6">
- {getFormFields()}
- </div>
- {/* 弹窗底部 */}
- <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
- <button
- onClick={() => setShowAddModal(false)}
- className="px-5 py-2.5 text-sm text-gray-600 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
- >
- {t('common.cancel')}
- </button>
- <button
- onClick={() => {
- console.log('保存:', editingItem);
- setShowAddModal(false);
- }}
- className="px-5 py-2.5 text-sm text-white bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all"
- >
- {editingItem ? t('common.saveChanges') : t('common.confirm')}
- </button>
- </div>
- </div>
- </div>
- )}
- {/* 查看详情弹框(只读模式,三个tab) */}
- <Modal
- title={null}
- open={showViewModal}
- onCancel={() => {
- setShowViewModal(false);
- setViewWorkDetailData(null);
- setViewWorkJobStep(0);
- viewWorkJobBasicForm.resetFields();
- viewWorkJobPublishForm.resetFields();
- setViewWorkflowJson(null);
- }}
- footer={null}
- width={1400}
- destroyOnClose
- styles={{
- body: { padding: 0 }
- }}
- >
- <div className="flex flex-col h-[80vh]">
- {/* Tab导航 */}
- <div className="px-6 py-4 bg-white">
- <Tabs
- activeKey={String(viewWorkJobStep)}
- onChange={(key) => {
- // 查看模式下可以自由切换tab
- setViewWorkJobStep(Number(key));
- }}
- items={[
- {
- key: '0',
- label: (
- <span className="flex items-center gap-2">
- <FileText className="w-4 h-4" />
- 基本信息
- </span>
- ),
- },
- {
- key: '1',
- label: (
- <span className="flex items-center gap-2">
- <Workflow className="w-4 h-4" />
- 流程管理
- </span>
- ),
- },
- {
- key: '2',
- label: (
- <span className="flex items-center gap-2">
- <Play className="w-4 h-4" />
- 发布作业
- </span>
- ),
- },
- ]}
- />
- </div>
- {/* Tab内容 */}
- <div className="flex-1 overflow-y-auto px-6 py-4 bg-white">
- {viewWorkJobStep === 0 && (
- <div className="flex justify-center">
- <Form
- form={viewWorkJobBasicForm}
- layout="vertical"
- className="max-w-2xl w-full"
- >
- <Form.Item
- label="流程模板"
- name="workflowTemplate"
- >
- <Select
- disabled
- placeholder="请选择流程模板"
- options={workflowTemplateList
- .filter(item => item.status === 1)
- .map(item => ({
- label: item.name,
- value: item.id,
- }))}
- />
- </Form.Item>
- <Form.Item
- label="作业分类"
- name="jobCategory"
- >
- <Select disabled placeholder="请选择作业分类">
- {workTypeDictList.map((item) => (
- <Select.Option key={item.id} value={item.value}>
- {item.label}
- </Select.Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item
- label="作业名称"
- name="jobName"
- >
- <Input disabled placeholder="请输入作业名称" />
- </Form.Item>
- <Form.Item
- label="作业内容"
- name="jobContent"
- >
- <Input.TextArea
- disabled
- rows={4}
- placeholder="请输入作业内容"
- showCount
- maxLength={500}
- />
- </Form.Item>
- <Form.Item
- label="紧急程度"
- name="urgency"
- >
- <Radio.Group disabled>
- {urgencyLevelDictList.length > 0 ? (
- urgencyLevelDictList.map((item) => (
- <Radio key={item.id} value={item.value}>
- {item.label}
- </Radio>
- ))
- ) : (
- <span className="text-gray-400 text-sm">加载中...</span>
- )}
- </Radio.Group>
- </Form.Item>
- </Form>
- </div>
- )}
- {viewWorkJobStep === 1 && (
- <div className="flex items-center justify-center h-full text-gray-400">
- {viewWorkflowJson ? '流程管理视图(只读模式)' : '暂无流程设计数据'}
- </div>
- )}
- {viewWorkJobStep === 2 && (
- <Form
- form={viewWorkJobPublishForm}
- layout="vertical"
- className="max-w-2xl"
- >
- <div className="mb-4">
- <h3 className="text-base font-medium text-gray-900 mb-2">发布作业设置</h3>
- <p className="text-sm text-gray-500">请配置作业的执行时间并最终发布</p>
- </div>
- <Form.Item
- label="发布方式"
- name="startType"
- >
- <Radio.Group disabled>
- <Radio value="0">立即发布</Radio>
- <Radio value="1">定时发布</Radio>
- </Radio.Group>
- </Form.Item>
- <Form.Item
- noStyle
- shouldUpdate={(prevValues, currentValues) =>
- prevValues.startType !== currentValues.startType
- }
- >
- {({ getFieldValue }) =>
- getFieldValue('startType') === '1' ? (
- <Form.Item
- label="发布时间"
- name="planTime"
- >
- <DatePicker
- disabled
- showTime
- format="YYYY-MM-DD HH:mm:ss"
- className="w-full"
- placeholder="请选择发布时间"
- />
- </Form.Item>
- ) : null
- }
- </Form.Item>
- </Form>
- )}
- </div>
- {/* 底部按钮 */}
- <div className="px-6 py-4 border-t border-gray-200 flex justify-end gap-2">
- <Button onClick={() => {
- setShowViewModal(false);
- setViewWorkDetailData(null);
- setViewWorkJobStep(0);
- viewWorkJobBasicForm.resetFields();
- viewWorkJobPublishForm.resetFields();
- setViewWorkflowJson(null);
- }}>
- 关闭
- </Button>
- </div>
- </div>
- </Modal>
- </div>
- );
- }
|