IsolationWork.tsx 309 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534
  1. import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { useTranslation } from 'react-i18next';
  4. import { Plus, Search, Edit2, Trash2, MoreVertical, FileText, Eye, Play, CheckCircle, RefreshCw, Workflow, Hand } from 'lucide-react';
  5. import { Button, Input, Space, Select, Table as AntdTable, message, Modal, Form, Row, Col, Tabs, Radio, DatePicker, Checkbox, Tooltip, Switch as AntdSwitch } from 'antd';
  6. import { Button as UIButton } from './ui/button';
  7. import type { ColumnsType } from 'antd/es/table';
  8. import { workflowDesignApi, WorkflowDesignVO } from '../api/WorkflowDesign';
  9. import { workJobApi, WorkJobVO, WorkflowWorkNodeDO, UpdateWorkflowWorkNodeParam, WorkflowWorkNodeUserDO, UpdateStartWorkParam } from '../api/WorkJob';
  10. import { dictDataApi, DictDataVO } from '../api/DictData';
  11. import ReactFlow, {
  12. Node,
  13. Edge,
  14. useNodesState,
  15. useEdgesState,
  16. Controls,
  17. Background,
  18. NodeTypes,
  19. BackgroundVariant,
  20. Handle,
  21. Position,
  22. ConnectionMode,
  23. BaseEdge,
  24. getStraightPath,
  25. EdgeTypes,
  26. } from 'reactflow';
  27. import 'reactflow/dist/style.css';
  28. import {
  29. ToolOutlined,
  30. CheckCircleOutlined,
  31. FileTextOutlined,
  32. EditOutlined,
  33. SafetyOutlined,
  34. UnlockOutlined,
  35. LockOutlined,
  36. CheckSquareOutlined,
  37. CloseOutlined,
  38. DeleteOutlined,
  39. ExclamationCircleOutlined,
  40. ClockCircleOutlined,
  41. FireOutlined,
  42. WarningOutlined,
  43. } from '@ant-design/icons';
  44. import { userApi, UserVO } from '../api/user';
  45. import { segregationPointApi, SegregationPointVO } from '../api/spm';
  46. import { getFormPage, getForm, FormVO } from '../api/bpm/form';
  47. import urgecy1Icon from '../assets/urgecy1.png';
  48. import urgecy2Icon from '../assets/urgecy2.png';
  49. import urgecy3Icon from '../assets/urgecy3.png';
  50. interface TableRow {
  51. id: number;
  52. [key: string]: any;
  53. }
  54. interface IsolationWorkProps {
  55. subMenu: string;
  56. }
  57. // 节点配置(从ProcessDesigner复制)
  58. const nodeConfigs = [
  59. {
  60. type: 'createJob',
  61. label: '创建作业',
  62. icon: ToolOutlined,
  63. bgColor: 'bg-white',
  64. bgColorCustom: '#ffffff',
  65. iconColor: 'text-blue-600',
  66. iconColorCustom: '#165dff',
  67. borderColor: 'border-blue-100',
  68. },
  69. {
  70. type: 'confirm',
  71. label: '确认',
  72. icon: CheckCircleOutlined,
  73. bgColor: 'bg-white',
  74. bgColorCustom: '#ffffff',
  75. iconColor: 'text-green-600',
  76. iconColorCustom: '#36d399',
  77. borderColor: 'border-green-100',
  78. },
  79. {
  80. type: 'review',
  81. label: '审核',
  82. icon: FileTextOutlined,
  83. bgColor: 'bg-white',
  84. bgColorCustom: '#ffffff',
  85. iconColor: 'text-orange-600',
  86. iconColorCustom: '#fb923c',
  87. borderColor: 'border-orange-100',
  88. },
  89. {
  90. type: 'inputInfo',
  91. label: '录入信息',
  92. icon: EditOutlined,
  93. bgColor: 'bg-white',
  94. bgColorCustom: '#ffffff',
  95. iconColor: 'text-purple-600',
  96. iconColorCustom: '#9665ff',
  97. borderColor: 'border-purple-100',
  98. },
  99. {
  100. type: 'isolation',
  101. label: '隔离/方案',
  102. icon: SafetyOutlined,
  103. bgColor: 'bg-white',
  104. bgColorCustom: '#ffffff',
  105. iconColor: 'text-red-600',
  106. iconColorCustom: '#f87272',
  107. borderColor: 'border-red-100',
  108. },
  109. {
  110. type: 'releaseIsolation',
  111. label: '解除隔离',
  112. icon: UnlockOutlined,
  113. bgColor: 'bg-white',
  114. bgColorCustom: '#ffffff',
  115. iconColor: 'text-yellow-600',
  116. iconColorCustom: '#38bdf8',
  117. borderColor: 'border-yellow-100',
  118. },
  119. {
  120. type: 'returnLock',
  121. label: '还锁',
  122. icon: LockOutlined,
  123. bgColor: 'bg-white',
  124. bgColorCustom: '#ffffff',
  125. iconColor: 'text-indigo-600',
  126. iconColorCustom: '#6b7280',
  127. borderColor: 'border-indigo-100',
  128. },
  129. {
  130. type: 'complete',
  131. label: '完成/结束',
  132. icon: CheckSquareOutlined,
  133. bgColor: 'bg-white',
  134. bgColorCustom: '#ffffff',
  135. iconColor: 'text-gray-600',
  136. iconColorCustom: '#10b981',
  137. borderColor: 'border-gray-100',
  138. },
  139. ];
  140. // 使用 import.meta.glob 动态导入所有图标(用于节点显示)
  141. const iconModulesForNode = import.meta.glob('../assets/节点图标/**/*.png', { eager: true, as: 'url' });
  142. // 根据文件名获取图标路径(用于节点显示)
  143. const getIconPathByFileName = (fileName: string | undefined): string | null => {
  144. if (!fileName) return null;
  145. // 如果已经是完整路径,尝试提取文件名
  146. let actualFileName = fileName;
  147. if (fileName.includes('/') || fileName.includes('\\')) {
  148. // 从路径中提取文件名
  149. const pathParts = fileName.replace(/\\/g, '/').split('/');
  150. actualFileName = pathParts[pathParts.length - 1];
  151. }
  152. // 从文件名中提取数字和分类
  153. const match = actualFileName.match(/^(\d+)\.png$/);
  154. if (!match) return null;
  155. const iconNumber = parseInt(match[1], 10);
  156. // 根据数字范围判断分类
  157. let category = '';
  158. if (iconNumber >= 1000 && iconNumber <= 1011) category = '审核';
  159. else if (iconNumber >= 2000 && iconNumber <= 2016) category = '开始';
  160. else if (iconNumber >= 3000 && iconNumber <= 3028) category = '录入';
  161. else if (iconNumber >= 4000 && iconNumber <= 4024) category = '确认';
  162. else if (iconNumber >= 5000 && iconNumber <= 5018) category = '结束';
  163. else if (iconNumber >= 6000 && iconNumber <= 6027) category = '能量隔离';
  164. else if (iconNumber >= 7000 && iconNumber <= 7021) category = '解除隔离';
  165. if (!category) return null;
  166. // 查找对应的路径
  167. const allKeys = Object.keys(iconModulesForNode);
  168. const matchingKey = allKeys.find(k => {
  169. const normalizedKey = k.replace(/\\/g, '/').toLowerCase();
  170. return normalizedKey.includes(category.toLowerCase()) && normalizedKey.endsWith(actualFileName.toLowerCase());
  171. });
  172. if (matchingKey) {
  173. return iconModulesForNode[matchingKey] as string;
  174. }
  175. return null;
  176. };
  177. // 自定义节点组件
  178. function CustomNode({ data, selected, id, nodeSavedStatusCache }: any) {
  179. const config = nodeConfigs.find(c => c.type === data.type);
  180. const Icon = config?.icon || FileTextOutlined;
  181. const nodeId = data.nodeId || (id ? String(parseInt(id.split('-').pop() || '0') % 1000).padStart(3, '0') : '001');
  182. // 优先使用缓存中的保存状态,如果没有则使用 data.completed
  183. const isSaved = nodeSavedStatusCache ? (nodeSavedStatusCache.get(id) || false) : (data.completed || false);
  184. const isCompleted = data.completed || false;
  185. // 检查是否是图片文件名(如 "1000.png")
  186. let iconImagePath: string | null = null;
  187. // 支持多种格式:纯文件名(如 "1000.png")或完整路径
  188. if (data.icon) {
  189. // 如果是纯文件名格式
  190. if (/^\d+\.png$/.test(data.icon)) {
  191. iconImagePath = getIconPathByFileName(data.icon);
  192. }
  193. // 如果已经是完整路径(兼容旧数据)
  194. else if (data.icon.includes('/') || data.icon.includes('\\')) {
  195. // 尝试从路径中提取文件名
  196. const pathParts = data.icon.replace(/\\/g, '/').split('/');
  197. const fileName = pathParts[pathParts.length - 1];
  198. if (/^\d+\.png$/.test(fileName)) {
  199. iconImagePath = getIconPathByFileName(fileName);
  200. }
  201. }
  202. }
  203. // 确定边框颜色和粗细:选中 > 已完成 > 默认
  204. // 选中:蓝色边框,加粗
  205. // 未选中但已完成:绿色边框,加粗
  206. // 未选中且未完成:灰色边框,加粗
  207. let borderClass = 'border-gray-400';
  208. let borderStyle: React.CSSProperties = { borderWidth: '3px' };
  209. let shadowStyle = {};
  210. if (selected) {
  211. // 选中的节点:蓝色边框,加粗
  212. borderClass = 'border-blue-500 shadow-md ring-2 ring-blue-300';
  213. borderStyle = { borderWidth: '4px' };
  214. shadowStyle = { boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)' };
  215. } else if (isSaved) {
  216. // 未选中但已保存的节点:绿色边框,加粗
  217. borderClass = 'border-green-500';
  218. borderStyle = { borderWidth: '3px' };
  219. shadowStyle = { boxShadow: '0 0 0 1px rgba(34, 197, 94, 0.1)' };
  220. } else {
  221. // 未选中且未保存的节点:灰色边框,加粗
  222. borderClass = 'border-gray-400';
  223. borderStyle = { borderWidth: '3px' };
  224. shadowStyle = {};
  225. }
  226. return (
  227. <div
  228. className={`relative px-4 py-4 rounded-lg shadow-sm w-[180px] h-auto min-h-[140px] bg-white ${borderClass} transition-all`}
  229. style={{ ...borderStyle, ...shadowStyle }}
  230. >
  231. <Handle
  232. id="top-source"
  233. type="source"
  234. position={Position.Top}
  235. className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
  236. style={{ top: -6, left: '50%', transform: 'translateX(-50%)' }}
  237. isConnectable={true}
  238. />
  239. <Handle
  240. id="top-target"
  241. type="target"
  242. position={Position.Top}
  243. className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
  244. style={{ top: -6, left: '50%', transform: 'translateX(-50%)' }}
  245. isConnectable={true}
  246. />
  247. <Handle
  248. id="bottom-source"
  249. type="source"
  250. position={Position.Bottom}
  251. className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
  252. style={{ bottom: -6, left: '50%', transform: 'translateX(-50%)' }}
  253. isConnectable={true}
  254. />
  255. <Handle
  256. id="bottom-target"
  257. type="target"
  258. position={Position.Bottom}
  259. className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
  260. style={{ bottom: -6, left: '50%', transform: 'translateX(-50%)' }}
  261. isConnectable={true}
  262. />
  263. <Handle
  264. id="left-source"
  265. type="source"
  266. position={Position.Left}
  267. className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
  268. style={{ left: -6, top: '50%', transform: 'translateY(-50%)' }}
  269. isConnectable={true}
  270. />
  271. <Handle
  272. id="left-target"
  273. type="target"
  274. position={Position.Left}
  275. className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
  276. style={{ left: -6, top: '50%', transform: 'translateY(-50%)' }}
  277. isConnectable={true}
  278. />
  279. <Handle
  280. id="right-source"
  281. type="source"
  282. position={Position.Right}
  283. className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
  284. style={{ right: -6, top: '50%', transform: 'translateY(-50%)' }}
  285. isConnectable={true}
  286. />
  287. <Handle
  288. id="right-target"
  289. type="target"
  290. position={Position.Right}
  291. className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
  292. style={{ right: -6, top: '50%', transform: 'translateY(-50%)' }}
  293. isConnectable={true}
  294. />
  295. <div className="flex flex-col items-center justify-between gap-3 h-full">
  296. <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' }}>
  297. {iconImagePath ? (
  298. <img
  299. src={iconImagePath}
  300. alt={data.icon || '节点图标'}
  301. className="w-6 h-6 object-contain"
  302. onError={(e) => {
  303. // 如果图片加载失败,回退到默认图标
  304. console.error('节点图标加载失败:', iconImagePath);
  305. (e.target as HTMLImageElement).style.display = 'none';
  306. }}
  307. />
  308. ) : (
  309. <Icon
  310. className={`${config?.iconColor || 'text-gray-600'} text-2xl`}
  311. style={{ color: config?.iconColorCustom || undefined }}
  312. />
  313. )}
  314. </div>
  315. <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">
  316. {data.label || config?.label}
  317. </div>
  318. <div className="text-xs text-gray-500 text-center flex-shrink-0">
  319. ID: {nodeId}
  320. </div>
  321. {/* 保存状态显示 */}
  322. <div className="flex items-center justify-center mt-2 mb-0 flex-shrink-0">
  323. {isSaved ? (
  324. <div className="flex items-center gap-1.5 text-green-600">
  325. <CheckCircleOutlined style={{ fontSize: '20px', color: '#22c55e' }} />
  326. <span className="text-xs font-medium">已配置</span>
  327. </div>
  328. ) : (
  329. <div className="flex items-center gap-1.5 text-gray-400">
  330. <ClockCircleOutlined style={{ fontSize: '18px' }} />
  331. <span className="text-xs">未配置</span>
  332. </div>
  333. )}
  334. </div>
  335. </div>
  336. </div>
  337. );
  338. }
  339. // 节点类型映射(将在组件内部重新定义以访问状态)
  340. // 自定义边组件
  341. function CustomEdgeWithDelete({
  342. id,
  343. sourceX,
  344. sourceY,
  345. targetX,
  346. targetY,
  347. selected,
  348. markerEnd,
  349. markerStart,
  350. style,
  351. }: any) {
  352. const [edgePath] = getStraightPath({
  353. sourceX,
  354. sourceY,
  355. targetX,
  356. targetY,
  357. });
  358. return (
  359. <>
  360. <BaseEdge
  361. id={id}
  362. path={edgePath}
  363. markerEnd={markerEnd}
  364. markerStart={markerStart}
  365. style={{
  366. ...style,
  367. strokeWidth: selected ? 3 : 2,
  368. stroke: selected ? '#3b82f6' : (style?.stroke || '#000000'),
  369. }}
  370. />
  371. </>
  372. );
  373. }
  374. const edgeTypes: EdgeTypes = {
  375. straight: CustomEdgeWithDelete,
  376. default: CustomEdgeWithDelete,
  377. };
  378. export default function IsolationWork({ subMenu }: IsolationWorkProps) {
  379. const { t, i18n } = useTranslation();
  380. const navigate = useNavigate();
  381. const [searchTerm, setSearchTerm] = useState('');
  382. const [showAddModal, setShowAddModal] = useState(false);
  383. const [editingItem, setEditingItem] = useState<TableRow | null>(null);
  384. const [form] = Form.useForm();
  385. // 发起作业多步骤表单状态
  386. const [workJobStep, setWorkJobStep] = useState(0); // 0: 基本信息, 1: 流程管理, 2: 发布作业
  387. const [workJobBasicForm] = Form.useForm(); // 基本信息表单
  388. const [workJobPublishForm] = Form.useForm(); // 发布作业表单
  389. const [workflowJson, setWorkflowJson] = useState<any>(null); // 流程设计JSON
  390. const [workflowWorkId, setWorkflowWorkId] = useState<number | null>(null); // 作业票ID(新增后返回)
  391. const [workflowWorkNodeDOList, setWorkflowWorkNodeDOList] = useState<WorkflowWorkNodeDO[]>([]); // 作业节点数据列表
  392. const [originalBasicFormData, setOriginalBasicFormData] = useState<any>(null); // 编辑时的原始表单数据(用于判断是否有修改)
  393. // 查看详情弹框状态(只读模式)
  394. const [showViewModal, setShowViewModal] = useState(false);
  395. const [viewWorkJobStep, setViewWorkJobStep] = useState(0); // 0: 基本信息, 1: 流程管理, 2: 发布作业
  396. const [viewWorkJobBasicForm] = Form.useForm(); // 查看模式基本信息表单
  397. const [viewWorkJobPublishForm] = Form.useForm(); // 查看模式发布作业表单
  398. const [viewWorkflowJson, setViewWorkflowJson] = useState<any>(null); // 查看模式流程设计JSON
  399. const [viewWorkDetailData, setViewWorkDetailData] = useState<any>(null); // 查看模式详情数据
  400. const [isViewMode, setIsViewMode] = useState(false); // 是否为查看模式(只读)
  401. // ReactFlow相关状态(用于流程管理tab)
  402. const [workflowNodes, setWorkflowNodes, onWorkflowNodesChange] = useNodesState([]);
  403. const [workflowEdges, setWorkflowEdges, onWorkflowEdgesChange] = useEdgesState([]);
  404. const [selectedWorkflowNode, setSelectedWorkflowNode] = useState<Node | null>(null);
  405. const [workflowActiveTabKey, setWorkflowActiveTabKey] = useState<string>('info');
  406. const workflowReactFlowWrapper = useRef<HTMLDivElement>(null);
  407. const [workflowReactFlowInstance, setWorkflowReactFlowInstance] = useState<any>(null);
  408. // 节点配置状态
  409. const [workflowNodeConfig, setWorkflowNodeConfig] = useState({
  410. nodeName: '',
  411. nodeIcon: '',
  412. responsible: undefined as number | string | undefined,
  413. remark: '',
  414. submitForm: undefined as number | string | undefined,
  415. isolationType: '',
  416. isolationPoints: [] as string[],
  417. isolationNode: [] as string[],
  418. isolationNodeUuid: '',
  419. lockPerson: undefined as number | string | undefined,
  420. coLockPersons: [] as (number | string)[],
  421. notificationMethods: {
  422. sms: false,
  423. message: false,
  424. email: false,
  425. app: false,
  426. },
  427. notificationPerson: '',
  428. notificationTime: '',
  429. smsTemplateCode: 'false',
  430. messageTemplateCode: 'false',
  431. emailTemplateCode: 'false',
  432. appTemplateCode: 'false',
  433. });
  434. // 节点配置缓存(Map<nodeId, config>)
  435. const [workflowNodeConfigCache, setWorkflowNodeConfigCache] = useState<Map<string, any>>(new Map());
  436. // 已完成的节点ID集合
  437. const [completedNodeIds, setCompletedNodeIds] = useState<Set<string>>(new Set());
  438. // 节点保存状态缓存(Map<nodeId, boolean>)- 用于记录当前作业中哪些节点已保存
  439. const [nodeSavedStatusCache, setNodeSavedStatusCache] = useState<Map<string, boolean>>(new Map());
  440. // 计算是否有未保存的节点(用于控制流程管理tab的下一步按钮)
  441. const hasUnsavedNodes = useMemo(() => {
  442. if (workflowNodes.length === 0) {
  443. return false; // 如果没有节点,认为没有未保存的节点
  444. }
  445. // 检查所有节点是否都已保存
  446. return workflowNodes.some(node => {
  447. const isSaved = nodeSavedStatusCache.get(node.id) || false;
  448. return !isSaved;
  449. });
  450. }, [workflowNodes, nodeSavedStatusCache]);
  451. // 节点类型映射(使用 useMemo 避免每次渲染都创建新对象)
  452. const nodeTypes: NodeTypes = useMemo(() => ({
  453. createJob: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
  454. confirm: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
  455. review: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
  456. inputInfo: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
  457. isolation: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
  458. releaseIsolation: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
  459. returnLock: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
  460. complete: (props: any) => <CustomNode {...props} nodeSavedStatusCache={nodeSavedStatusCache} />,
  461. }), [nodeSavedStatusCache]);
  462. // 角色用户列表和表单列表
  463. const [workflowDrawerUsers, setWorkflowDrawerUsers] = useState<UserVO[]>([]);
  464. const [workflowLockerUsers, setWorkflowLockerUsers] = useState<UserVO[]>([]);
  465. const [workflowColockerUsers, setWorkflowColockerUsers] = useState<UserVO[]>([]);
  466. const [workflowIsolationPoints, setWorkflowIsolationPoints] = useState<SegregationPointVO[]>([]);
  467. const [workflowFormList, setWorkflowFormList] = useState<FormVO[]>([]);
  468. // 流程设计列表数据(用于流程设计页面)
  469. const [processDesignList, setProcessDesignList] = useState<WorkflowDesignVO[]>([]);
  470. const [processDesignTotal, setProcessDesignTotal] = useState(0);
  471. const [processDesignLoading, setProcessDesignLoading] = useState(false);
  472. const [processDesignStatusUpdating, setProcessDesignStatusUpdating] = useState<Record<number, boolean>>({});
  473. // 作业分类字典数据
  474. const [workTypeDictList, setWorkTypeDictList] = useState<DictDataVO[]>([]);
  475. // 紧急程度字典数据
  476. const [urgencyLevelDictList, setUrgencyLevelDictList] = useState<DictDataVO[]>([]);
  477. // 隔离方式字典数据
  478. const [isolationTypeDictList, setIsolationTypeDictList] = useState<DictDataVO[]>([]);
  479. const [jobStatusDictList, setJobStatusDictList] = useState<DictDataVO[]>([]);
  480. // 流程设计列表数据(用于流程模板下拉框)
  481. const [workflowTemplateList, setWorkflowTemplateList] = useState<WorkflowDesignVO[]>([]);
  482. // 作业管理列表数据
  483. const [workJobList, setWorkJobList] = useState<WorkJobVO[]>([]);
  484. const [workJobTotal, setWorkJobTotal] = useState(0);
  485. const [workJobLoading, setWorkJobLoading] = useState(false);
  486. // 流程模板数据
  487. const templateData: TableRow[] = [
  488. {
  489. id: 1,
  490. code: 'TPL-001',
  491. name: '高压设备隔离流程',
  492. type: '标准流程',
  493. category: '高压作业',
  494. steps: 8,
  495. approvalLevel: '二级审批',
  496. creator: '张三',
  497. createTime: '2024-01-10',
  498. status: '启用',
  499. useCount: 45,
  500. remark: '适用于10kV及以上高压设备'
  501. },
  502. {
  503. id: 2,
  504. code: 'TPL-002',
  505. name: '低压配电柜隔离流程',
  506. type: '标准流程',
  507. category: '低压作业',
  508. steps: 6,
  509. approvalLevel: '一级审批',
  510. creator: '李四',
  511. createTime: '2024-01-15',
  512. status: '启用',
  513. useCount: 128,
  514. remark: '适用于380V配电柜'
  515. },
  516. {
  517. id: 3,
  518. code: 'TPL-003',
  519. name: '紧急抢修隔离流程',
  520. type: '应急流程',
  521. category: '应急处理',
  522. steps: 5,
  523. approvalLevel: '特殊审批',
  524. creator: '王五',
  525. createTime: '2024-02-01',
  526. status: '启用',
  527. useCount: 12,
  528. remark: '用于紧急故障抢修'
  529. },
  530. {
  531. id: 4,
  532. code: 'TPL-004',
  533. name: '定期维护隔离流程',
  534. type: '标准流程',
  535. category: '定期维护',
  536. steps: 7,
  537. approvalLevel: '一级审批',
  538. creator: '张三',
  539. createTime: '2024-02-10',
  540. status: '启用',
  541. useCount: 67,
  542. remark: '用于定期维护保养'
  543. },
  544. ];
  545. // SOP管理数据
  546. const sopData: TableRow[] = [
  547. {
  548. id: 1,
  549. code: 'SOP-001',
  550. name: '高压开关操作规程',
  551. version: 'V2.1',
  552. category: '操作规程',
  553. department: '技术部',
  554. effectiveDate: '2024-01-01',
  555. reviewDate: '2025-01-01',
  556. status: '有效',
  557. reviewer: '李四',
  558. lastUpdate: '2024-12-01',
  559. attachments: 2,
  560. remark: '包含视频培训资料'
  561. },
  562. {
  563. id: 2,
  564. code: 'SOP-002',
  565. name: '配电柜断电操作规程',
  566. version: 'V1.5',
  567. category: '操作规程',
  568. department: '运维部',
  569. effectiveDate: '2024-02-01',
  570. reviewDate: '2025-02-01',
  571. status: '有效',
  572. reviewer: '张三',
  573. lastUpdate: '2024-11-15',
  574. attachments: 1,
  575. remark: ''
  576. },
  577. {
  578. id: 3,
  579. code: 'SOP-003',
  580. name: '接地线安装规程',
  581. version: 'V3.0',
  582. category: '安全规程',
  583. department: '安全部',
  584. effectiveDate: '2024-01-15',
  585. reviewDate: '2025-01-15',
  586. status: '有效',
  587. reviewer: '王五',
  588. lastUpdate: '2024-12-03',
  589. attachments: 3,
  590. remark: '新增便携式接地线章节'
  591. },
  592. {
  593. id: 4,
  594. code: 'SOP-004',
  595. name: '验电作业规程',
  596. version: 'V2.0',
  597. category: '安全规程',
  598. department: '安全部',
  599. effectiveDate: '2024-03-01',
  600. reviewDate: '2025-03-01',
  601. status: '待审核',
  602. reviewer: '赵六',
  603. lastUpdate: '2024-11-30',
  604. attachments: 1,
  605. remark: '待技术部审核'
  606. },
  607. ];
  608. // 表单管理数据
  609. const formManagementData: TableRow[] = [
  610. {
  611. id: 1,
  612. name: '高压作业申请表',
  613. creator: '张三',
  614. createTime: '2024-01-10 14:30',
  615. version: 'V1.0',
  616. status: '启用',
  617. description: '用于高压设备作业申请的表单',
  618. useCount: 45
  619. },
  620. {
  621. id: 2,
  622. name: '低压作业申请表',
  623. creator: '李四',
  624. createTime: '2024-01-15 09:20',
  625. version: 'V1.2',
  626. status: '启用',
  627. description: '用于低压设备作业申请的表单',
  628. useCount: 128
  629. },
  630. {
  631. id: 3,
  632. name: '紧急抢修申请表',
  633. creator: '王五',
  634. createTime: '2024-02-01 16:45',
  635. version: 'V2.0',
  636. status: '启用',
  637. description: '用于紧急故障抢修申请的表单',
  638. useCount: 12
  639. },
  640. {
  641. id: 4,
  642. name: '定期维护申请表',
  643. creator: '张三',
  644. createTime: '2024-02-10 11:15',
  645. version: 'V1.5',
  646. status: '停用',
  647. description: '用于定期维护申请的表单',
  648. useCount: 67
  649. },
  650. ];
  651. // 表单管理搜索参数
  652. const [formManagementQuery, setFormManagementQuery] = useState({
  653. name: '',
  654. status: undefined as string | undefined,
  655. });
  656. const [formManagementPagination, setFormManagementPagination] = useState({
  657. pageNo: 1,
  658. pageSize: 10,
  659. });
  660. // 流程设计数据
  661. const processDesignData: TableRow[] = [
  662. {
  663. id: 1,
  664. name: '高压设备隔离流程设计',
  665. designer: '张三',
  666. designTime: '2024-01-10 14:30',
  667. nodeCount: 8,
  668. description: '适用于10kV及以上高压设备的隔离作业流程',
  669. status: '启用'
  670. },
  671. {
  672. id: 2,
  673. name: '低压配电柜隔离流程设计',
  674. designer: '李四',
  675. designTime: '2024-01-15 09:20',
  676. nodeCount: 6,
  677. description: '适用于380V配电柜的隔离作业流程',
  678. status: '启用'
  679. },
  680. {
  681. id: 3,
  682. name: '紧急抢修隔离流程设计',
  683. designer: '王五',
  684. designTime: '2024-02-01 16:45',
  685. nodeCount: 5,
  686. description: '用于紧急故障抢修的快速隔离流程',
  687. status: '启用'
  688. },
  689. {
  690. id: 4,
  691. name: '定期维护隔离流程设计',
  692. designer: '张三',
  693. designTime: '2024-02-10 11:15',
  694. nodeCount: 7,
  695. description: '用于定期维护保养的隔离流程',
  696. status: '停用'
  697. },
  698. ];
  699. // 流程设计搜索参数
  700. const [processDesignQuery, setProcessDesignQuery] = useState({
  701. name: '',
  702. });
  703. const [processDesignPagination, setProcessDesignPagination] = useState({
  704. pageNo: 1,
  705. pageSize: 10,
  706. });
  707. // 作业管理搜索参数
  708. // 从 sessionStorage 读取初始 status
  709. const getInitialWorkStatus = () => {
  710. const status = sessionStorage.getItem('workManagementStatus');
  711. return status || undefined;
  712. };
  713. const [workJobQuery, setWorkJobQuery] = useState({
  714. name: '',
  715. status: getInitialWorkStatus() as string | undefined,
  716. });
  717. const [workJobPagination, setWorkJobPagination] = useState({
  718. pageNo: 1,
  719. pageSize: 10,
  720. });
  721. // 作业管理数据(旧数据,保留用于兼容)
  722. const workData: TableRow[] = [
  723. {
  724. id: 1,
  725. code: 'WORK-2025-001',
  726. name: '1号变压器定期检修',
  727. type: '定期维护',
  728. template: '定期维护隔离流程',
  729. location: 'B区地下室变压器房',
  730. equipment: '变压器柜A',
  731. applicant: '李四',
  732. department: '技术部',
  733. planStartTime: '2025-12-10 08:00',
  734. planEndTime: '2025-12-10 18:00',
  735. status: '待审批',
  736. approver: '王五',
  737. priority: '普通',
  738. remark: '年度例行检修'
  739. },
  740. {
  741. id: 2,
  742. code: 'WORK-2025-002',
  743. name: '2号配电柜故障抢修',
  744. type: '应急处理',
  745. template: '紧急抢修隔离流程',
  746. location: 'A区2层配电室',
  747. equipment: '2号配电柜',
  748. applicant: '张三',
  749. department: '运维部',
  750. planStartTime: '2025-12-04 14:00',
  751. planEndTime: '2025-12-04 18:00',
  752. status: '进行中',
  753. approver: '李四',
  754. priority: '紧急',
  755. remark: '断路器跳闸需紧急处理'
  756. },
  757. {
  758. id: 3,
  759. code: 'WORK-2025-003',
  760. name: 'A区照明回路维护',
  761. type: '定期维护',
  762. template: '低压配电柜隔离流程',
  763. location: 'A区1层配电室',
  764. equipment: '1号配电柜',
  765. applicant: '王五',
  766. department: '运维部',
  767. planStartTime: '2025-12-05 09:00',
  768. planEndTime: '2025-12-05 12:00',
  769. status: '已完成',
  770. approver: '张三',
  771. priority: '普通',
  772. remark: ''
  773. },
  774. {
  775. id: 4,
  776. code: 'WORK-2025-004',
  777. name: 'C区空调系统检查',
  778. type: '定期维护',
  779. template: '低压配电柜隔离流程',
  780. location: 'C区3层控制室',
  781. equipment: '控制柜C1',
  782. applicant: '赵六',
  783. department: '运维部',
  784. planStartTime: '2025-12-06 08:00',
  785. planEndTime: '2025-12-06 11:00',
  786. status: '已完成',
  787. approver: '张三',
  788. priority: '普通',
  789. remark: '冬季例行检查'
  790. },
  791. {
  792. id: 5,
  793. code: 'WORK-2025-005',
  794. name: '高压开关柜年检',
  795. type: '定期维护',
  796. template: '高压设备隔离流程',
  797. location: 'B区地下室变压器房',
  798. equipment: '变压器柜A',
  799. applicant: '李四',
  800. department: '技术部',
  801. planStartTime: '2025-12-15 08:00',
  802. planEndTime: '2025-12-15 17:00',
  803. status: '待审批',
  804. approver: '王五',
  805. priority: '重要',
  806. remark: '需第三方检测机构配合'
  807. },
  808. ];
  809. // 表单管理搜索处理
  810. const handleFormManagementQuery = () => {
  811. // 这里可以添加搜索逻辑
  812. console.log('搜索表单管理:', formManagementQuery);
  813. };
  814. const resetFormManagementQuery = () => {
  815. setFormManagementQuery({
  816. name: '',
  817. status: undefined,
  818. });
  819. };
  820. // 获取流程设计列表
  821. const getProcessDesignList = async () => {
  822. setProcessDesignLoading(true);
  823. try {
  824. const response = await workflowDesignApi.getWorkflowDesignPage({
  825. pageNo: processDesignPagination.pageNo,
  826. pageSize: processDesignPagination.pageSize,
  827. name: processDesignQuery.name || undefined,
  828. });
  829. setProcessDesignList(response.list || []);
  830. setProcessDesignTotal(response.total || 0);
  831. } catch (error: any) {
  832. console.error('获取流程设计列表失败:', error);
  833. message.error(error?.message || '获取流程设计列表失败');
  834. } finally {
  835. setProcessDesignLoading(false);
  836. }
  837. };
  838. // 获取流程设计列表(用于流程模板下拉框,获取全部数据)
  839. const getWorkflowTemplateList = async () => {
  840. try {
  841. const response = await workflowDesignApi.getWorkflowDesignPage({
  842. pageNo: 1,
  843. pageSize: -1,
  844. status: 1, // 只获取启用状态(status=1)的流程模板
  845. });
  846. setWorkflowTemplateList(response.list || []);
  847. } catch (error: any) {
  848. console.error('获取流程模板列表失败:', error);
  849. setWorkflowTemplateList([]);
  850. }
  851. };
  852. // 处理流程设计状态切换
  853. const handleProcessDesignStatusChanged = async (record: WorkflowDesignVO, newStatus: number) => {
  854. if (!record.id) {
  855. message.error('流程设计ID不存在');
  856. return;
  857. }
  858. // 设置该记录为更新中状态
  859. setProcessDesignStatusUpdating(prev => ({ ...prev, [record.id!]: true }));
  860. try {
  861. await workflowDesignApi.updateWorkflowDesignStatus({
  862. id: record.id,
  863. status: newStatus,
  864. });
  865. // 更新本地列表中的状态
  866. setProcessDesignList(prev =>
  867. prev.map(item =>
  868. item.id === record.id ? { ...item, status: newStatus } : item
  869. )
  870. );
  871. message.success(newStatus === 1 ? '已启用' : '已禁用');
  872. } catch (error: any) {
  873. console.error('更新流程设计状态失败:', error);
  874. message.error(error?.message || '更新状态失败');
  875. } finally {
  876. // 清除更新中状态
  877. setProcessDesignStatusUpdating(prev => {
  878. const newState = { ...prev };
  879. delete newState[record.id!];
  880. return newState;
  881. });
  882. }
  883. };
  884. // 获取作业分类字典数据
  885. const getWorkTypeDictList = async () => {
  886. try {
  887. const response = await dictDataApi.getDictDataPage({
  888. pageNo: 1,
  889. pageSize: -1,
  890. dictType: 'work_type',
  891. });
  892. console.log('作业分类字典API响应:', response);
  893. const data = (response as any)?.data || response;
  894. console.log('作业分类字典解析后的data:', data);
  895. const list = data?.list || [];
  896. console.log('作业分类字典数据list:', list);
  897. setWorkTypeDictList(list);
  898. } catch (error: any) {
  899. console.error('获取作业分类字典失败:', error);
  900. console.error('错误详情:', error?.response?.data || error);
  901. setWorkTypeDictList([]);
  902. }
  903. };
  904. // 获取紧急程度字典数据
  905. const getUrgencyLevelDictList = async () => {
  906. try {
  907. const response = await dictDataApi.getDictDataPage({
  908. pageNo: 1,
  909. pageSize: -1,
  910. dictType: 'urgency_level',
  911. });
  912. console.log('紧急程度字典API响应:', response);
  913. const data = (response as any)?.data || response;
  914. console.log('紧急程度字典解析后的data:', data);
  915. const list = data?.list || [];
  916. console.log('紧急程度字典数据list:', list);
  917. console.log('紧急程度字典数据list长度:', list.length);
  918. setUrgencyLevelDictList(list);
  919. // 如果有数据且表单还没有设置值,设置默认值为第一项
  920. if (list.length > 0 && !workJobBasicForm.getFieldValue('urgency')) {
  921. workJobBasicForm.setFieldsValue({ urgency: list[0].value });
  922. }
  923. } catch (error: any) {
  924. console.error('获取紧急程度字典失败:', error);
  925. console.error('错误详情:', error?.response?.data || error);
  926. setUrgencyLevelDictList([]);
  927. }
  928. };
  929. // 获取隔离方式字典数据
  930. const getIsolationMethodDictList = async () => {
  931. try {
  932. const response = await dictDataApi.getDictDataPage({
  933. pageNo: 1,
  934. pageSize: -1,
  935. dictType: 'isolation_method',
  936. });
  937. const data = (response as any)?.data || response;
  938. const list = data?.list || [];
  939. setIsolationTypeDictList(list);
  940. } catch (error: any) {
  941. console.error('获取隔离方式字典失败:', error);
  942. setIsolationTypeDictList([]);
  943. }
  944. };
  945. // 获取作业状态字典数据
  946. const getJobStatusDictList = async () => {
  947. try {
  948. const response = await dictDataApi.getDictDataPage({
  949. pageNo: 1,
  950. pageSize: -1,
  951. dictType: 'job_status',
  952. });
  953. const data = (response as any)?.data || response;
  954. const list = data?.list || [];
  955. setJobStatusDictList(list);
  956. } catch (error: any) {
  957. console.error('获取作业状态字典失败:', error);
  958. setJobStatusDictList([]);
  959. }
  960. };
  961. // 流程设计搜索处理
  962. const handleProcessDesignQuery = () => {
  963. setProcessDesignPagination({ ...processDesignPagination, pageNo: 1 });
  964. };
  965. const resetProcessDesignQuery = () => {
  966. setProcessDesignQuery({
  967. name: '',
  968. });
  969. setProcessDesignPagination({ pageNo: 1, pageSize: 10 });
  970. // 立即调用接口刷新数据
  971. getProcessDesignList();
  972. };
  973. // 监听菜单切换,重置状态并加载数据
  974. useEffect(() => {
  975. // 当切换菜单时,重置相关状态并加载数据
  976. if (subMenu === '流程设计') {
  977. // 重置分页到第一页
  978. setProcessDesignPagination({ pageNo: 1, pageSize: 10 });
  979. // 重置查询条件
  980. setProcessDesignQuery({ name: '' });
  981. // 加载数据
  982. getProcessDesignList();
  983. } else if (subMenu === '作业管理') {
  984. // 重置分页到第一页
  985. setWorkJobPagination({ pageNo: 1, pageSize: 10 });
  986. // 检查是否有从 sessionStorage 传入的 status
  987. const statusFromStorage = sessionStorage.getItem('workManagementStatus');
  988. // 重置查询条件(如果有 status 则保留,否则清空)
  989. setWorkJobQuery({ name: '', status: statusFromStorage || undefined });
  990. // 如果读取了 status,清除 sessionStorage
  991. if (statusFromStorage) {
  992. sessionStorage.removeItem('workManagementStatus');
  993. }
  994. // 加载数据
  995. getWorkJobList();
  996. // 加载作业分类字典数据
  997. getWorkTypeDictList();
  998. // 加载紧急程度字典数据
  999. getUrgencyLevelDictList();
  1000. // 加载作业状态字典数据
  1001. getJobStatusDictList();
  1002. // 加载隔离方式字典数据
  1003. getIsolationMethodDictList();
  1004. // 加载流程设计列表(用于流程模板下拉框)
  1005. getWorkflowTemplateList();
  1006. }
  1007. // eslint-disable-next-line react-hooks/exhaustive-deps
  1008. }, [subMenu]);
  1009. // 流程设计页面加载时获取列表
  1010. useEffect(() => {
  1011. if (subMenu === '流程设计') {
  1012. getProcessDesignList();
  1013. }
  1014. // eslint-disable-next-line react-hooks/exhaustive-deps
  1015. }, [processDesignPagination.pageNo, processDesignPagination.pageSize, processDesignQuery.name]);
  1016. // 获取作业管理列表
  1017. const getWorkJobList = async () => {
  1018. setWorkJobLoading(true);
  1019. try {
  1020. const response = await workJobApi.getWorkflowWorkPage({
  1021. pageNo: workJobPagination.pageNo,
  1022. pageSize: workJobPagination.pageSize,
  1023. name: workJobQuery.name || undefined,
  1024. status: workJobQuery.status || undefined,
  1025. });
  1026. setWorkJobList(response.list || []);
  1027. setWorkJobTotal(response.total || 0);
  1028. } catch (error: any) {
  1029. console.error('获取作业列表失败:', error);
  1030. message.error(error?.message || '获取作业列表失败');
  1031. setWorkJobList([]);
  1032. setWorkJobTotal(0);
  1033. } finally {
  1034. setWorkJobLoading(false);
  1035. }
  1036. };
  1037. // 作业管理搜索处理
  1038. const handleWorkJobQuery = () => {
  1039. setWorkJobPagination({ ...workJobPagination, pageNo: 1 });
  1040. };
  1041. const resetWorkJobQuery = () => {
  1042. setWorkJobQuery({
  1043. name: '',
  1044. status: undefined,
  1045. });
  1046. setWorkJobPagination({ pageNo: 1, pageSize: 10 });
  1047. };
  1048. // 组件挂载时从 sessionStorage 读取 status
  1049. useEffect(() => {
  1050. const status = sessionStorage.getItem('workManagementStatus');
  1051. if (status) {
  1052. console.log('从 sessionStorage 读取到 workManagementStatus:', status);
  1053. setWorkJobQuery(prev => ({
  1054. ...prev,
  1055. status: status,
  1056. }));
  1057. // 读取后清除 sessionStorage,避免下次进入时自动应用
  1058. sessionStorage.removeItem('workManagementStatus');
  1059. }
  1060. }, []);
  1061. // 作业管理页面加载时获取列表
  1062. useEffect(() => {
  1063. if (subMenu === '作业管理') {
  1064. getWorkJobList();
  1065. }
  1066. // eslint-disable-next-line react-hooks/exhaustive-deps
  1067. }, [workJobPagination.pageNo, workJobPagination.pageSize, workJobQuery.name, workJobQuery.status]);
  1068. // 加载流程设计JSON并渲染到ReactFlow
  1069. const loadWorkflowJson = async (workflowId: number) => {
  1070. try {
  1071. const response = await workflowDesignApi.selectWorkflowDesignById(workflowId);
  1072. console.log('流程设计详情API响应:', response);
  1073. // axios拦截器可能已经处理了数据格式,直接使用response
  1074. const workflow = response;
  1075. console.log('流程设计数据:', workflow);
  1076. // console.log('content字段:', workflow.content);
  1077. // 流程设计数据在 content 字段中(JSON字符串)
  1078. let jsonData: any = null;
  1079. // 检查 content 是否为 null 或空字符串
  1080. const hasContent = workflow.content && workflow.content !== null && workflow.content !== '';
  1081. if (hasContent) {
  1082. try {
  1083. jsonData = typeof workflow.content === 'string'
  1084. ? JSON.parse(workflow.content)
  1085. : workflow.content;
  1086. console.log('解析后的JSON数据:', jsonData);
  1087. setWorkflowJson(jsonData);
  1088. } catch (parseError) {
  1089. console.error('解析JSON失败:', parseError);
  1090. console.error('原始content:', workflow.content);
  1091. message.error('流程设计JSON格式错误');
  1092. // 解析失败时清空流程
  1093. setWorkflowJson(null);
  1094. setWorkflowNodes([]);
  1095. setWorkflowEdges([]);
  1096. setWorkflowNodeConfigCache(new Map());
  1097. setCompletedNodeIds(new Set());
  1098. setNodeSavedStatusCache(new Map());
  1099. setSelectedWorkflowNode(null);
  1100. return;
  1101. }
  1102. } else {
  1103. // content 为 null 或空时,清空流程,不使用备用数据
  1104. console.warn('流程设计content为空或null,清空流程');
  1105. setWorkflowJson(null);
  1106. setWorkflowNodes([]);
  1107. setWorkflowEdges([]);
  1108. setWorkflowNodeConfigCache(new Map());
  1109. setCompletedNodeIds(new Set());
  1110. setNodeSavedStatusCache(new Map());
  1111. setSelectedWorkflowNode(null);
  1112. return;
  1113. }
  1114. // 解析并渲染到ReactFlow
  1115. if (jsonData && jsonData.nodes && Array.isArray(jsonData.nodes) && jsonData.edges && Array.isArray(jsonData.edges)) {
  1116. console.log('开始渲染节点和连线,节点数量:', jsonData.nodes.length, '连线数量:', jsonData.edges.length);
  1117. // 创建节点映射,方便后续查找已保存的节点
  1118. const nodeDOMap = new Map<string, WorkflowWorkNodeDO>();
  1119. workflowWorkNodeDOList.forEach((nodeDO: WorkflowWorkNodeDO) => {
  1120. if (nodeDO.uuid) {
  1121. nodeDOMap.set(nodeDO.uuid, nodeDO);
  1122. }
  1123. });
  1124. const importedNodes: Node[] = jsonData.nodes.map((node: any) => {
  1125. const nodeData = node.data || {};
  1126. const nodeId = node.id || node.uuid;
  1127. const nodeDO = nodeDOMap.get(nodeId);
  1128. // 获取图标:优先使用 nodeIcon(顶层),其次使用 data.icon
  1129. const iconValue = node.nodeIcon || nodeData.icon || '';
  1130. return {
  1131. id: nodeId,
  1132. type: node.type || 'createJob',
  1133. position: node.position || { x: 0, y: 0 },
  1134. data: (() => {
  1135. const { isolationMethod, ...restNodeData } = nodeData || {};
  1136. return {
  1137. ...restNodeData,
  1138. label: nodeData.label || node.label || node.nodeName || nodeConfigs.find(c => c.type === (node.type || nodeData.type))?.label || '节点',
  1139. type: node.type || nodeData.type || 'createJob',
  1140. icon: iconValue, // 确保icon字段被正确传递
  1141. // 标记节点是否已完成配置(如果 nodeDO 存在且有 id,说明已经保存过)
  1142. completed: !!nodeDO?.id,
  1143. };
  1144. })(),
  1145. };
  1146. });
  1147. const importedEdges: Edge[] = jsonData.edges.map((edge: any) => {
  1148. // 处理sourceHandle和targetHandle
  1149. // 如果JSON中有指定handle,使用指定的;否则根据连接方向推断
  1150. let sourceHandle = edge.sourceHandle;
  1151. let targetHandle = edge.targetHandle;
  1152. // 如果没有指定handle,尝试从edge的id或其他字段推断
  1153. // 例如:edge id可能是 "edge-confirm-xxx-right-source-createJob-xxx-left-target-xxx"
  1154. if (!sourceHandle && edge.id) {
  1155. const sourceMatch = edge.id.match(/right-source|left-source|top-source|bottom-source/);
  1156. if (sourceMatch) {
  1157. sourceHandle = sourceMatch[0].replace('-source', '');
  1158. } else {
  1159. // 默认使用右侧连接点
  1160. sourceHandle = 'right-source';
  1161. }
  1162. } else if (!sourceHandle) {
  1163. // 默认使用右侧连接点
  1164. sourceHandle = 'right-source';
  1165. }
  1166. if (!targetHandle && edge.id) {
  1167. const targetMatch = edge.id.match(/right-target|left-target|top-target|bottom-target/);
  1168. if (targetMatch) {
  1169. targetHandle = targetMatch[0].replace('-target', '');
  1170. } else {
  1171. // 默认使用左侧连接点
  1172. targetHandle = 'left-target';
  1173. }
  1174. } else if (!targetHandle) {
  1175. // 默认使用左侧连接点
  1176. targetHandle = 'left-target';
  1177. }
  1178. return {
  1179. id: edge.id || `${edge.source}-${edge.target}`,
  1180. source: edge.source,
  1181. target: edge.target,
  1182. sourceHandle: sourceHandle,
  1183. targetHandle: targetHandle,
  1184. type: 'straight',
  1185. style: { strokeWidth: 2, stroke: '#000000' },
  1186. markerStart: {
  1187. type: 'arrowclosed',
  1188. color: '#000000',
  1189. },
  1190. };
  1191. });
  1192. console.log('导入的节点:', importedNodes);
  1193. console.log('导入的连线:', importedEdges);
  1194. setWorkflowNodes(importedNodes);
  1195. setWorkflowEdges(importedEdges);
  1196. // 清空之前的缓存和完成状态
  1197. setWorkflowNodeConfigCache(new Map());
  1198. setCompletedNodeIds(new Set());
  1199. // 清除节点保存状态缓存
  1200. setNodeSavedStatusCache(new Map());
  1201. // 自动选中第一个节点
  1202. if (importedNodes.length > 0) {
  1203. const firstNode = importedNodes[0];
  1204. setTimeout(() => {
  1205. setSelectedWorkflowNode(firstNode);
  1206. const source = firstNode.data || {};
  1207. const config = nodeConfigs.find(c => c.type === source.type);
  1208. setWorkflowNodeConfig({
  1209. nodeName: source.label || config?.label || '',
  1210. nodeIcon: source.icon || '',
  1211. responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
  1212. remark: source.remark || '',
  1213. submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
  1214. isolationType: source.isolationType || '',
  1215. isolationPoints: source.isolationPoints || [],
  1216. isolationNode: source.isolationNode || [],
  1217. isolationNodeUuid: source.isolationNodeUuid || '',
  1218. lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
  1219. coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
  1220. notificationMethods: source.notificationMethods || {
  1221. sms: false,
  1222. message: false,
  1223. email: false,
  1224. app: false,
  1225. },
  1226. notificationPerson: source.notificationPerson || '',
  1227. notificationTime: source.notificationTime || '',
  1228. });
  1229. // 高亮第一个节点
  1230. setWorkflowNodes((nds) =>
  1231. nds.map((node) =>
  1232. node.id === firstNode.id
  1233. ? { ...node, selected: true }
  1234. : { ...node, selected: false }
  1235. )
  1236. );
  1237. }, 200);
  1238. }
  1239. } else {
  1240. console.error('JSON数据格式错误,缺少nodes或edges:', jsonData);
  1241. message.error('流程设计数据格式错误:缺少节点或连线数据');
  1242. }
  1243. } catch (error: any) {
  1244. console.error('加载流程设计失败:', error);
  1245. console.error('错误详情:', error?.response?.data || error);
  1246. message.error('加载流程设计失败: ' + (error?.message || '未知错误'));
  1247. // 使用备用JSON
  1248. const fallbackJson = {
  1249. nodes: [
  1250. { id: '1', type: 'createJob', label: '提交/开始', position: { x: 100, y: 100 } },
  1251. { id: '2', type: 'review', label: '审核/确认', position: { x: 300, y: 100 } },
  1252. { id: '3', type: 'inputInfo', label: '录入/表单', position: { x: 500, y: 100 } },
  1253. { id: '4', type: 'isolation', label: '隔离/方案', position: { x: 700, y: 100 } },
  1254. { id: '5', type: 'releaseIsolation', label: '取锁/共锁', position: { x: 700, y: 300 } },
  1255. { id: '6', type: 'complete', label: '完成/结束', position: { x: 900, y: 300 } },
  1256. ],
  1257. edges: [
  1258. { id: 'e1-2', source: '1', target: '2' },
  1259. { id: 'e2-3', source: '2', target: '3' },
  1260. { id: 'e3-4', source: '3', target: '4' },
  1261. { id: 'e4-5', source: '4', target: '5' },
  1262. { id: 'e5-6', source: '5', target: '6' },
  1263. { id: 'e2-5', source: '2', target: '5' },
  1264. ],
  1265. };
  1266. setWorkflowJson(fallbackJson);
  1267. const importedNodes: Node[] = fallbackJson.nodes.map((node: any) => ({
  1268. id: node.id,
  1269. type: node.type,
  1270. position: node.position,
  1271. data: {
  1272. label: node.label,
  1273. type: node.type,
  1274. icon: node.icon || node.nodeIcon || '', // 确保icon字段被传递
  1275. },
  1276. }));
  1277. const importedEdges: Edge[] = fallbackJson.edges.map((edge: any) => ({
  1278. id: edge.id,
  1279. source: edge.source,
  1280. target: edge.target,
  1281. type: 'straight',
  1282. style: { strokeWidth: 2, stroke: '#000000' },
  1283. markerStart: {
  1284. type: 'arrowclosed',
  1285. color: '#000000',
  1286. },
  1287. }));
  1288. setWorkflowNodes(importedNodes);
  1289. setWorkflowEdges(importedEdges);
  1290. }
  1291. };
  1292. // 加载角色用户列表和表单列表(用于节点配置)
  1293. useEffect(() => {
  1294. if (workJobStep === 1) {
  1295. const loadRoleUsers = async () => {
  1296. try {
  1297. const [drawerRes, lockerRes, colockerRes] = await Promise.all([
  1298. userApi.getRoleUser('jtdrawer'),
  1299. userApi.getRoleUser('jtlocker'),
  1300. userApi.getRoleUser('jtcolocker'),
  1301. ]);
  1302. setWorkflowDrawerUsers(drawerRes || []);
  1303. setWorkflowLockerUsers(lockerRes || []);
  1304. setWorkflowColockerUsers(colockerRes || []);
  1305. } catch (error) {
  1306. console.error('加载角色用户失败:', error);
  1307. }
  1308. };
  1309. const loadIsolationPoints = async () => {
  1310. try {
  1311. const res = await segregationPointApi.getIsIsolationPointPage({ pageNo: 1, pageSize: -1 });
  1312. setWorkflowIsolationPoints(res.list || []);
  1313. } catch (error) {
  1314. console.error('加载隔离点列表失败:', error);
  1315. }
  1316. };
  1317. const loadFormList = async () => {
  1318. try {
  1319. // 只获取开启状态(status=0)的表单
  1320. const res = await getFormPage({ pageNo: 1, pageSize: -1, status: 0 });
  1321. setWorkflowFormList(res.list || []);
  1322. } catch (error) {
  1323. console.error('加载表单列表失败:', error);
  1324. }
  1325. };
  1326. // 初始化节点状态:调用 checkWorkById 和 selectWorkflowWorkById 接口
  1327. const initializeNodeStates = async () => {
  1328. if (!workflowWorkId) {
  1329. console.warn('workflowWorkId 不存在,无法初始化节点状态');
  1330. return;
  1331. }
  1332. try {
  1333. // 1. 调用 checkWorkById 获取未保存的节点列表
  1334. const checkResponse = await workJobApi.checkWorkById(workflowWorkId);
  1335. const checkData = (checkResponse as any)?.data || checkResponse;
  1336. const unsavedNodeMessages = Array.isArray(checkData) ? checkData : (checkData ? [checkData] : []);
  1337. // 2. 调用 selectWorkflowWorkById 获取所有节点数据
  1338. const detailResponse = await workJobApi.selectWorkflowWorkById(workflowWorkId);
  1339. const detail = detailResponse as any;
  1340. if (detail.workflowWorkNodeDOList && Array.isArray(detail.workflowWorkNodeDOList)) {
  1341. // 更新 workflowWorkNodeDOList
  1342. setWorkflowWorkNodeDOList(detail.workflowWorkNodeDOList);
  1343. // 3. 根据接口数据初始化缓存和节点状态
  1344. // 创建新的缓存和状态,不使用旧的(因为我们要用接口数据覆盖)
  1345. const newCache = new Map<string, any>();
  1346. const newCompleted = new Set<string>();
  1347. const newSavedStatusCache = new Map<string, boolean>();
  1348. // 遍历所有节点,初始化缓存
  1349. detail.workflowWorkNodeDOList.forEach((nodeDO: WorkflowWorkNodeDO) => {
  1350. if (!nodeDO.uuid) return;
  1351. // 解析节点数据
  1352. let nodeData: any = {};
  1353. if (nodeDO.data) {
  1354. try {
  1355. nodeData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
  1356. } catch (e) {
  1357. console.error('解析节点data失败:', e);
  1358. }
  1359. }
  1360. // 构建节点配置
  1361. const config = nodeConfigs.find(c => c.type === (nodeDO.type || nodeData.type));
  1362. const nodeConfig = {
  1363. nodeName: nodeDO.nodeName || nodeData.label || config?.label || '',
  1364. nodeIcon: nodeDO.nodeIcon || nodeData.icon || '',
  1365. responsible: (nodeDO.workerUserId !== null && nodeDO.workerUserId !== undefined && nodeDO.workerUserId !== 0)
  1366. ? (typeof nodeDO.workerUserId === 'number' ? nodeDO.workerUserId : Number(nodeDO.workerUserId))
  1367. : (nodeData.workerUserId && nodeData.workerUserId !== '' && nodeData.workerUserId !== '0')
  1368. ? (typeof nodeData.workerUserId === 'number' ? nodeData.workerUserId : Number(nodeData.workerUserId))
  1369. : undefined,
  1370. remark: nodeData.remark || '',
  1371. submitForm: nodeDO.formId ? (typeof nodeDO.formId === 'number' ? nodeDO.formId : Number(nodeDO.formId)) : (nodeData.submitForm ? (typeof nodeData.submitForm === 'number' ? nodeData.submitForm : Number(nodeData.submitForm)) : undefined),
  1372. isolationType: (nodeDO.isolationType !== null && nodeDO.isolationType !== undefined) ? String(nodeDO.isolationType) : (nodeData.isolationType || ''),
  1373. isolationPoints: nodeDO.isolationPoints ? (typeof nodeDO.isolationPoints === 'string' ? JSON.parse(nodeDO.isolationPoints) : nodeDO.isolationPoints) : (nodeData.isolationPoints || []),
  1374. isolationNode: nodeData.isolationNode || [],
  1375. isolationNodeUuid: nodeDO.isolationNodeUuid || nodeData.isolationNodeUuid || '',
  1376. lockPerson: (() => {
  1377. // 优先从 nodeUserList 中提取
  1378. let lockPersonId: number | undefined = undefined;
  1379. if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
  1380. const lockerUser = nodeDO.nodeUserList.find((user: any) => user.type === 'jtlocker');
  1381. if (lockerUser?.userId) {
  1382. lockPersonId = typeof lockerUser.userId === 'number' ? lockerUser.userId : Number(lockerUser.userId);
  1383. }
  1384. }
  1385. return lockPersonId || (nodeData.lockPerson ? (typeof nodeData.lockPerson === 'number' ? nodeData.lockPerson : Number(nodeData.lockPerson)) : undefined);
  1386. })(),
  1387. coLockPersons: (() => {
  1388. // 优先从 nodeUserList 中提取
  1389. const coLockPersonIds: number[] = [];
  1390. if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
  1391. nodeDO.nodeUserList.forEach((user: any) => {
  1392. if (user.type === 'jtcolocker' && user.userId) {
  1393. coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
  1394. }
  1395. });
  1396. }
  1397. return coLockPersonIds.length > 0 ? coLockPersonIds : (nodeData.coLockPersons && Array.isArray(nodeData.coLockPersons) ? nodeData.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
  1398. })(),
  1399. notificationMethods: nodeData.notificationMethods || {
  1400. sms: false,
  1401. message: false,
  1402. email: false,
  1403. app: false,
  1404. },
  1405. notificationPerson: nodeData.notificationPerson || '',
  1406. notificationTime: nodeDO.notifyTime || nodeData.notificationTime || '',
  1407. smsTemplateCode: nodeData.smsTemplateCode || (nodeData.notificationMethods?.sms ? 'true' : 'false'),
  1408. messageTemplateCode: nodeData.messageTemplateCode || (nodeData.notificationMethods?.message ? 'true' : 'false'),
  1409. emailTemplateCode: nodeData.emailTemplateCode || (nodeData.notificationMethods?.email ? 'true' : 'false'),
  1410. appTemplateCode: nodeData.appTemplateCode || (nodeData.notificationMethods?.app ? 'true' : 'false'),
  1411. };
  1412. // 将节点配置存入缓存
  1413. newCache.set(nodeDO.uuid, nodeConfig);
  1414. // 检查节点是否已保存(如果不在未保存列表中,说明已保存)
  1415. // 通过检查未保存消息中是否包含该节点的信息来判断
  1416. const isNodeUnsaved = unsavedNodeMessages.some((msg: string) => {
  1417. // 如果消息中包含节点名称或UUID,说明该节点未保存
  1418. return msg.includes(nodeDO.nodeName || '') || msg.includes(nodeDO.uuid || '');
  1419. });
  1420. if (!isNodeUnsaved) {
  1421. // 节点已保存,标记为已完成
  1422. newCompleted.add(nodeDO.uuid);
  1423. newSavedStatusCache.set(nodeDO.uuid, true);
  1424. } else {
  1425. // 节点未保存
  1426. newSavedStatusCache.set(nodeDO.uuid, false);
  1427. }
  1428. });
  1429. // 更新缓存和状态
  1430. setWorkflowNodeConfigCache(newCache);
  1431. setCompletedNodeIds(newCompleted);
  1432. setNodeSavedStatusCache(newSavedStatusCache);
  1433. console.log('节点状态初始化完成:', {
  1434. totalNodes: detail.workflowWorkNodeDOList.length,
  1435. completedNodes: newCompleted.size,
  1436. unsavedNodes: unsavedNodeMessages.length,
  1437. });
  1438. // 找到第一个未保存的节点并自动选中(按照流程顺序)
  1439. // 等待 workflowNodes 渲染完成后再选中
  1440. // 使用 setState 的回调形式确保获取最新的 workflowNodes
  1441. setTimeout(() => {
  1442. setWorkflowNodes((currentNodes) => {
  1443. // 使用 getNextIncompleteNode 的逻辑,但基于 nodeSavedStatusCache 来判断
  1444. // 找到第一个未保存的节点(按照流程顺序)
  1445. let firstUnsavedNode: Node | null = null;
  1446. if (workflowJson && workflowJson.adjacency && currentNodes.length > 0) {
  1447. const adjacency = workflowJson.adjacency;
  1448. const nodeMap = new Map<string, Node>();
  1449. // 初始化节点映射
  1450. currentNodes.forEach(node => {
  1451. nodeMap.set(node.id, node);
  1452. });
  1453. // 辅助函数:检查节点的所有父节点是否都已保存
  1454. const areAllParentsSaved = (nodeId: string): boolean => {
  1455. const nodeInfo = adjacency[nodeId];
  1456. if (!nodeInfo || !nodeInfo.parentUuid || nodeInfo.parentUuid.length === 0) {
  1457. return true; // 没有父节点,可以执行
  1458. }
  1459. return nodeInfo.parentUuid.every((parentId: string) => {
  1460. const isParentSaved = newSavedStatusCache.get(parentId);
  1461. return isParentSaved === true; // 父节点已保存
  1462. });
  1463. };
  1464. // 辅助函数:获取节点的第一个未保存的子节点
  1465. const getFirstUnsavedChild = (nodeId: string): string | null => {
  1466. const nodeInfo = adjacency[nodeId];
  1467. if (!nodeInfo || !nodeInfo.childrenUuid || nodeInfo.childrenUuid.length === 0) {
  1468. return null; // 没有子节点
  1469. }
  1470. // 按顺序查找第一个未保存的子节点
  1471. for (const childId of nodeInfo.childrenUuid) {
  1472. const isChildSaved = newSavedStatusCache.get(childId);
  1473. if (isChildSaved === false && areAllParentsSaved(childId)) {
  1474. return childId;
  1475. }
  1476. }
  1477. return null;
  1478. };
  1479. // BFS遍历查找第一个未保存的节点
  1480. const queue: string[] = [];
  1481. const visited = new Set<string>();
  1482. // 找到所有开始节点(parentUuid 为空的节点)
  1483. for (const nodeId in adjacency) {
  1484. const nodeInfo = adjacency[nodeId];
  1485. if (!nodeInfo.parentUuid || nodeInfo.parentUuid.length === 0) {
  1486. const isSaved = newSavedStatusCache.get(nodeId);
  1487. if (isSaved === false) {
  1488. // 如果开始节点未保存,直接返回
  1489. firstUnsavedNode = nodeMap.get(nodeId) || null;
  1490. if (firstUnsavedNode) {
  1491. break;
  1492. }
  1493. } else {
  1494. // 如果开始节点已保存,将其子节点加入队列
  1495. if (nodeInfo.childrenUuid && nodeInfo.childrenUuid.length > 0) {
  1496. nodeInfo.childrenUuid.forEach((childId: string) => {
  1497. if (!visited.has(childId)) {
  1498. queue.push(childId);
  1499. visited.add(childId);
  1500. }
  1501. });
  1502. }
  1503. }
  1504. }
  1505. }
  1506. // 如果开始节点都已保存,BFS遍历查找下一个未保存的节点
  1507. if (!firstUnsavedNode) {
  1508. while (queue.length > 0) {
  1509. const currentNodeId = queue.shift()!;
  1510. const isSaved = newSavedStatusCache.get(currentNodeId);
  1511. if (isSaved === false && areAllParentsSaved(currentNodeId)) {
  1512. firstUnsavedNode = nodeMap.get(currentNodeId) || null;
  1513. if (firstUnsavedNode) {
  1514. break;
  1515. }
  1516. }
  1517. // 如果当前节点已保存,将其子节点加入队列
  1518. const nodeInfo = adjacency[currentNodeId];
  1519. if (nodeInfo && nodeInfo.childrenUuid && nodeInfo.childrenUuid.length > 0) {
  1520. nodeInfo.childrenUuid.forEach((childId: string) => {
  1521. if (!visited.has(childId)) {
  1522. queue.push(childId);
  1523. visited.add(childId);
  1524. }
  1525. });
  1526. }
  1527. }
  1528. }
  1529. } else {
  1530. // 如果没有 adjacency 信息,回退到简单查找
  1531. firstUnsavedNode = currentNodes.find(node => {
  1532. const isSaved = newSavedStatusCache.get(node.id);
  1533. return isSaved === false; // 明确为 false 表示未保存
  1534. }) || null;
  1535. }
  1536. if (!firstUnsavedNode) {
  1537. console.log('没有找到未保存的节点,所有节点都已保存');
  1538. return currentNodes;
  1539. }
  1540. // 选中第一个未保存的节点
  1541. setSelectedWorkflowNode(firstUnsavedNode);
  1542. // 从缓存中获取节点配置
  1543. const cachedConfig = newCache.get(firstUnsavedNode.id);
  1544. if (cachedConfig) {
  1545. setWorkflowNodeConfig(cachedConfig);
  1546. } else {
  1547. // 如果缓存中没有,从 nodeDO 中获取
  1548. const nodeDO = detail.workflowWorkNodeDOList.find(item => item.uuid === firstUnsavedNode.id);
  1549. if (nodeDO) {
  1550. let nodeData: any = {};
  1551. if (nodeDO.data) {
  1552. try {
  1553. nodeData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
  1554. } catch (e) {
  1555. console.error('解析节点data失败:', e);
  1556. }
  1557. }
  1558. const config = nodeConfigs.find(c => c.type === (nodeDO.type || nodeData.type));
  1559. setWorkflowNodeConfig({
  1560. nodeName: nodeDO.nodeName || nodeData.label || config?.label || '',
  1561. nodeIcon: nodeDO.nodeIcon || nodeData.icon || '',
  1562. responsible: (nodeDO.workerUserId !== null && nodeDO.workerUserId !== undefined && nodeDO.workerUserId !== 0)
  1563. ? (typeof nodeDO.workerUserId === 'number' ? nodeDO.workerUserId : Number(nodeDO.workerUserId))
  1564. : (nodeData.workerUserId && nodeData.workerUserId !== '' && nodeData.workerUserId !== '0')
  1565. ? (typeof nodeData.workerUserId === 'number' ? nodeData.workerUserId : Number(nodeData.workerUserId))
  1566. : undefined,
  1567. remark: nodeData.remark || '',
  1568. submitForm: nodeDO.formId ? (typeof nodeDO.formId === 'number' ? nodeDO.formId : Number(nodeDO.formId)) : (nodeData.submitForm ? (typeof nodeData.submitForm === 'number' ? nodeData.submitForm : Number(nodeData.submitForm)) : undefined),
  1569. isolationType: (nodeDO.isolationType !== null && nodeDO.isolationType !== undefined) ? String(nodeDO.isolationType) : (nodeData.isolationType || ''),
  1570. isolationPoints: nodeDO.isolationPoints ? (typeof nodeDO.isolationPoints === 'string' ? JSON.parse(nodeDO.isolationPoints) : nodeDO.isolationPoints) : (nodeData.isolationPoints || []),
  1571. isolationNode: nodeData.isolationNode || [],
  1572. isolationNodeUuid: nodeDO.isolationNodeUuid || nodeData.isolationNodeUuid || '',
  1573. lockPerson: (() => {
  1574. let lockPersonId: number | undefined = undefined;
  1575. if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
  1576. const lockerUser = nodeDO.nodeUserList.find((user: any) => user.type === 'jtlocker');
  1577. if (lockerUser?.userId) {
  1578. lockPersonId = typeof lockerUser.userId === 'number' ? lockerUser.userId : Number(lockerUser.userId);
  1579. }
  1580. }
  1581. return lockPersonId || (nodeData.lockPerson ? (typeof nodeData.lockPerson === 'number' ? nodeData.lockPerson : Number(nodeData.lockPerson)) : undefined);
  1582. })(),
  1583. coLockPersons: (() => {
  1584. const coLockPersonIds: number[] = [];
  1585. if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
  1586. nodeDO.nodeUserList.forEach((user: any) => {
  1587. if (user.type === 'jtcolocker' && user.userId) {
  1588. coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
  1589. }
  1590. });
  1591. }
  1592. return coLockPersonIds.length > 0 ? coLockPersonIds : (nodeData.coLockPersons && Array.isArray(nodeData.coLockPersons) ? nodeData.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
  1593. })(),
  1594. notificationMethods: nodeData.notificationMethods || {
  1595. sms: false,
  1596. message: false,
  1597. email: false,
  1598. app: false,
  1599. },
  1600. notificationPerson: nodeData.notificationPerson || '',
  1601. notificationTime: nodeDO.notifyTime || nodeData.notificationTime || '',
  1602. });
  1603. } else {
  1604. // 如果既没有缓存也没有 nodeDO,使用节点原始数据
  1605. const source = firstUnsavedNode.data || {};
  1606. const config = nodeConfigs.find(c => c.type === source.type);
  1607. setWorkflowNodeConfig({
  1608. nodeName: source.label || config?.label || '',
  1609. nodeIcon: source.icon || '',
  1610. responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
  1611. remark: source.remark || '',
  1612. submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
  1613. isolationType: source.isolationType || '',
  1614. isolationPoints: source.isolationPoints || [],
  1615. isolationNode: source.isolationNode || [],
  1616. isolationNodeUuid: source.isolationNodeUuid || '',
  1617. lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
  1618. coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
  1619. notificationMethods: source.notificationMethods || {
  1620. sms: false,
  1621. message: false,
  1622. email: false,
  1623. app: false,
  1624. },
  1625. notificationPerson: source.notificationPerson || '',
  1626. notificationTime: source.notificationTime || '',
  1627. });
  1628. }
  1629. }
  1630. console.log('自动选中第一个未保存的节点:', firstUnsavedNode.id);
  1631. // 高亮选中的节点
  1632. return currentNodes.map((node) =>
  1633. node.id === firstUnsavedNode.id
  1634. ? { ...node, selected: true }
  1635. : { ...node, selected: false }
  1636. );
  1637. });
  1638. }, 300); // 延迟300ms,确保 workflowNodes 已经渲染完成
  1639. }
  1640. } catch (error: any) {
  1641. console.error('初始化节点状态失败:', error);
  1642. message.error('加载节点状态失败: ' + (error?.message || '未知错误'));
  1643. }
  1644. };
  1645. loadRoleUsers();
  1646. loadIsolationPoints();
  1647. loadFormList();
  1648. initializeNodeStates();
  1649. }
  1650. }, [workJobStep, workflowWorkId]);
  1651. // 验证节点配置是否完整
  1652. const validateNodeConfig = useCallback((config: any, nodeType: string): boolean => {
  1653. // 节点名称必填
  1654. if (!config.nodeName || config.nodeName.trim() === '') {
  1655. return false;
  1656. }
  1657. // 业务表单必填(只有确认节点、审核节点、录入信息节点需要必填)
  1658. const formRequiredNodeTypes = ['confirm', 'review', 'inputInfo'];
  1659. if (formRequiredNodeTypes.includes(nodeType) && !config.submitForm) {
  1660. return false;
  1661. }
  1662. // 负责人必填(创建作业、隔离、解除隔离节点除外)
  1663. // 对于隔离节点,负责人验证在隔离方式验证中处理
  1664. if (nodeType !== 'createJob' && nodeType !== 'isolation' && nodeType !== 'releaseIsolation') {
  1665. if (!config.responsible) {
  1666. return false;
  1667. }
  1668. }
  1669. // 隔离/方案节点特殊验证
  1670. if (nodeType === 'isolation') {
  1671. if (!config.isolationType || config.isolationType === '') {
  1672. return false;
  1673. }
  1674. if (config.isolationPoints.length === 0) {
  1675. return false;
  1676. }
  1677. // 字典值:0=盲板,1=上锁挂牌,2=拆除
  1678. if (config.isolationType === '0' || config.isolationType === '2') {
  1679. if (!config.responsible) {
  1680. return false;
  1681. }
  1682. }
  1683. if (config.isolationType === '1') {
  1684. if (!config.lockPerson) {
  1685. return false;
  1686. }
  1687. }
  1688. }
  1689. // 解除隔离节点特殊验证
  1690. if (nodeType === 'releaseIsolation') {
  1691. if (!config.isolationNodeUuid || config.isolationNodeUuid === '') {
  1692. return false;
  1693. }
  1694. }
  1695. return true;
  1696. }, []);
  1697. // 获取下一个未完成的节点(基于 workflowJson 中的 adjacency 信息)
  1698. // 规则:
  1699. // 1. 从开始节点(parentUuid 为空的节点)开始
  1700. // 2. 按照 adjacency 中的 childrenUuid 顺序执行
  1701. // 3. 如果有多个子节点,按顺序一个一个执行
  1702. // 4. 所有子节点完成后,再找下一个节点
  1703. const getNextIncompleteNode = useCallback((completedSet?: Set<string>, currentNodeId?: string): Node | null => {
  1704. const completed = completedSet || completedNodeIds;
  1705. // 如果所有节点都已完成,返回null
  1706. if (completed.size === workflowNodes.length) {
  1707. return null;
  1708. }
  1709. // 从 workflowJson 中获取 adjacency 信息
  1710. if (!workflowJson || !workflowJson.adjacency) {
  1711. // 如果没有 adjacency 信息,回退到使用边的逻辑
  1712. console.warn('workflowJson 中没有 adjacency 信息,使用边的逻辑');
  1713. return null;
  1714. }
  1715. const adjacency = workflowJson.adjacency;
  1716. const nodeMap = new Map<string, Node>(); // 节点ID -> 节点对象
  1717. // 初始化节点映射
  1718. workflowNodes.forEach(node => {
  1719. nodeMap.set(node.id, node);
  1720. });
  1721. // 辅助函数:检查节点的所有父节点是否都已完成
  1722. const areAllParentsCompleted = (nodeId: string): boolean => {
  1723. const nodeInfo = adjacency[nodeId];
  1724. if (!nodeInfo || !nodeInfo.parentUuid || nodeInfo.parentUuid.length === 0) {
  1725. return true; // 没有父节点,可以执行
  1726. }
  1727. return nodeInfo.parentUuid.every((parentId: string) => completed.has(parentId));
  1728. };
  1729. // 辅助函数:获取节点的第一个未完成的子节点
  1730. const getFirstIncompleteChild = (nodeId: string): string | null => {
  1731. const nodeInfo = adjacency[nodeId];
  1732. if (!nodeInfo || !nodeInfo.childrenUuid || nodeInfo.childrenUuid.length === 0) {
  1733. return null; // 没有子节点
  1734. }
  1735. // 按顺序查找第一个未完成的子节点
  1736. for (const childId of nodeInfo.childrenUuid) {
  1737. if (!completed.has(childId) && areAllParentsCompleted(childId)) {
  1738. return childId;
  1739. }
  1740. }
  1741. return null;
  1742. };
  1743. // 如果提供了当前节点ID,优先查找当前节点的下一个子节点
  1744. if (currentNodeId && adjacency[currentNodeId]) {
  1745. const nextChildId = getFirstIncompleteChild(currentNodeId);
  1746. if (nextChildId) {
  1747. const nextNode = nodeMap.get(nextChildId);
  1748. if (nextNode) {
  1749. return nextNode;
  1750. }
  1751. }
  1752. }
  1753. // 如果没有当前节点或当前节点的子节点都已完成,使用BFS查找下一个可执行的节点
  1754. const queue: string[] = [];
  1755. const visited = new Set<string>();
  1756. // 找到所有开始节点(parentUuid 为空的节点)
  1757. for (const nodeId in adjacency) {
  1758. const nodeInfo = adjacency[nodeId];
  1759. if (!nodeInfo.parentUuid || nodeInfo.parentUuid.length === 0) {
  1760. if (!completed.has(nodeId)) {
  1761. // 如果开始节点未完成,直接返回
  1762. const node = nodeMap.get(nodeId);
  1763. if (node) {
  1764. return node;
  1765. }
  1766. } else {
  1767. // 如果开始节点已完成,将其子节点加入队列
  1768. if (nodeInfo.childrenUuid && nodeInfo.childrenUuid.length > 0) {
  1769. nodeInfo.childrenUuid.forEach((childId: string) => {
  1770. if (!visited.has(childId)) {
  1771. queue.push(childId);
  1772. visited.add(childId);
  1773. }
  1774. });
  1775. }
  1776. }
  1777. }
  1778. }
  1779. // BFS遍历查找下一个可执行的未完成节点
  1780. while (queue.length > 0) {
  1781. const currentNodeId = queue.shift()!;
  1782. // 如果当前节点未完成,检查是否可执行(所有父节点都已完成)
  1783. if (!completed.has(currentNodeId) && areAllParentsCompleted(currentNodeId)) {
  1784. const node = nodeMap.get(currentNodeId);
  1785. if (node) {
  1786. return node;
  1787. }
  1788. }
  1789. // 如果当前节点已完成,将其子节点加入队列(按顺序)
  1790. if (completed.has(currentNodeId)) {
  1791. const nodeInfo = adjacency[currentNodeId];
  1792. if (nodeInfo && nodeInfo.childrenUuid && nodeInfo.childrenUuid.length > 0) {
  1793. nodeInfo.childrenUuid.forEach((childId: string) => {
  1794. if (!visited.has(childId)) {
  1795. queue.push(childId);
  1796. visited.add(childId);
  1797. }
  1798. });
  1799. }
  1800. }
  1801. }
  1802. return null;
  1803. }, [workflowNodes, workflowEdges, completedNodeIds, workflowJson]);
  1804. // 节点点击事件
  1805. const onWorkflowNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
  1806. console.log('点击节点:', node.id);
  1807. // 更新节点选中状态
  1808. setWorkflowNodes((nds) =>
  1809. nds.map((n) =>
  1810. n.id === node.id
  1811. ? { ...n, selected: true }
  1812. : { ...n, selected: false }
  1813. )
  1814. );
  1815. // 先设置选中的节点,确保右侧面板显示
  1816. setSelectedWorkflowNode(node);
  1817. // 优先从 workflowWorkNodeDOList 中查找对应节点数据(通过uuid匹配)
  1818. const nodeDO = workflowWorkNodeDOList.find(item => item.uuid === node.id);
  1819. console.log('点击节点:', node.id, '找到的 nodeDO:', nodeDO);
  1820. console.log('当前 workflowWorkNodeDOList:', workflowWorkNodeDOList);
  1821. if (nodeDO) {
  1822. // 如果找到节点数据,解析data字段并回显
  1823. let nodeData: any = {};
  1824. if (nodeDO.data) {
  1825. try {
  1826. nodeData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
  1827. } catch (e) {
  1828. console.error('解析节点data失败:', e);
  1829. }
  1830. }
  1831. // 从 nodeUserList 中提取上锁人和共锁人
  1832. let lockPersonId: string | number = '';
  1833. const coLockPersonIds: (string | number)[] = [];
  1834. if (nodeDO.nodeUserList && Array.isArray(nodeDO.nodeUserList)) {
  1835. nodeDO.nodeUserList.forEach((user: any) => {
  1836. if (user.type === 'jtlocker' && user.userId) {
  1837. lockPersonId = typeof user.userId === 'number' ? user.userId : Number(user.userId);
  1838. } else if (user.type === 'jtcolocker' && user.userId) {
  1839. coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
  1840. }
  1841. });
  1842. }
  1843. // 优先使用 nodeDO 中的数据(编辑时从后端获取的真实数据)
  1844. // 如果 nodeDO 存在且有 id,说明是已保存的节点,优先使用 nodeDO 的数据
  1845. const source = nodeData || node.data || {};
  1846. const config = nodeConfigs.find(c => c.type === (nodeDO.type || source.type));
  1847. // 解析隔离点
  1848. let isolationPoints: string[] = [];
  1849. if (nodeDO.isolationPoints) {
  1850. try {
  1851. isolationPoints = typeof nodeDO.isolationPoints === 'string'
  1852. ? JSON.parse(nodeDO.isolationPoints)
  1853. : (Array.isArray(nodeDO.isolationPoints) ? nodeDO.isolationPoints : []);
  1854. } catch (e) {
  1855. console.error('解析隔离点失败:', e);
  1856. isolationPoints = source.isolationPoints || [];
  1857. }
  1858. } else {
  1859. isolationPoints = source.isolationPoints || [];
  1860. }
  1861. // 构建节点配置,优先使用 nodeDO 中的数据
  1862. console.log('构建节点配置 - nodeDO.workerUserId:', nodeDO.workerUserId, 'source.workerUserId:', source.workerUserId);
  1863. const nodeConfig = {
  1864. nodeName: nodeDO.nodeName || source.label || config?.label || '',
  1865. nodeIcon: nodeDO.nodeIcon || source.icon || '',
  1866. responsible: (nodeDO.workerUserId !== null && nodeDO.workerUserId !== undefined && nodeDO.workerUserId !== 0)
  1867. ? (typeof nodeDO.workerUserId === 'number' ? nodeDO.workerUserId : Number(nodeDO.workerUserId))
  1868. : (source.workerUserId && source.workerUserId !== '' && source.workerUserId !== '0')
  1869. ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId))
  1870. : undefined,
  1871. remark: source.remark || '',
  1872. submitForm: nodeDO.formId ? (typeof nodeDO.formId === 'number' ? nodeDO.formId : Number(nodeDO.formId)) : (source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined),
  1873. isolationType: (nodeDO.isolationType !== null && nodeDO.isolationType !== undefined) ? String(nodeDO.isolationType) : (source.isolationType || ''),
  1874. isolationPoints: isolationPoints,
  1875. isolationNode: source.isolationNode || [],
  1876. isolationNodeUuid: nodeDO.isolationNodeUuid || source.isolationNodeUuid || '',
  1877. lockPerson: lockPersonId || (source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined),
  1878. coLockPersons: coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []),
  1879. notificationMethods: source.notificationMethods || {
  1880. sms: false,
  1881. message: false,
  1882. email: false,
  1883. app: false,
  1884. },
  1885. notificationPerson: source.notificationPerson || '',
  1886. notificationTime: nodeDO.notifyTime || source.notificationTime || '',
  1887. smsTemplateCode: source.smsTemplateCode || 'false',
  1888. messageTemplateCode: source.messageTemplateCode || 'false',
  1889. emailTemplateCode: source.emailTemplateCode || 'false',
  1890. appTemplateCode: source.appTemplateCode || 'false',
  1891. };
  1892. // 优先使用缓存中的配置(如果有),这样可以保持用户未保存的修改
  1893. // 如果缓存中没有,再使用 nodeDO 的数据
  1894. console.log('设置节点配置 - nodeConfig.responsible:', nodeConfig.responsible, 'node.id:', node.id);
  1895. const cachedConfig = workflowNodeConfigCache.get(node.id);
  1896. console.log('缓存中的配置:', cachedConfig);
  1897. if (cachedConfig) {
  1898. // 如果缓存中有配置,使用缓存(可能包含用户未保存的修改)
  1899. console.log('使用缓存配置');
  1900. // 使用函数式更新确保状态正确更新
  1901. setWorkflowNodeConfig(() => ({ ...cachedConfig }));
  1902. } else if (nodeDO.id) {
  1903. // 如果缓存中没有,但节点已保存,使用 nodeDO 的数据
  1904. console.log('使用 nodeDO 配置');
  1905. setWorkflowNodeConfig(() => ({ ...nodeConfig }));
  1906. } else {
  1907. // 如果既没有缓存也没有保存,使用节点数据
  1908. console.log('使用节点数据配置');
  1909. setWorkflowNodeConfig(() => ({ ...nodeConfig }));
  1910. }
  1911. } else {
  1912. // 如果没有找到节点数据,使用原有逻辑
  1913. console.log('没有找到 nodeDO,使用节点数据');
  1914. const cachedConfig = workflowNodeConfigCache.get(node.id);
  1915. if (cachedConfig) {
  1916. console.log('使用缓存配置(无 nodeDO)');
  1917. setWorkflowNodeConfig(() => cachedConfig);
  1918. } else {
  1919. console.log('使用节点原始数据');
  1920. const source = node.data || {};
  1921. const config = nodeConfigs.find(c => c.type === source.type);
  1922. setWorkflowNodeConfig({
  1923. nodeName: source.label || config?.label || '',
  1924. nodeIcon: source.icon || '',
  1925. responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
  1926. remark: source.remark || '',
  1927. submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
  1928. isolationType: source.isolationType || '',
  1929. isolationPoints: source.isolationPoints || [],
  1930. isolationNode: source.isolationNode || [],
  1931. isolationNodeUuid: source.isolationNodeUuid || '',
  1932. lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
  1933. coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : [],
  1934. notificationMethods: source.notificationMethods || {
  1935. sms: false,
  1936. message: false,
  1937. email: false,
  1938. app: false,
  1939. },
  1940. notificationPerson: source.notificationPerson || '',
  1941. notificationTime: source.notificationTime || '',
  1942. smsTemplateCode: source.smsTemplateCode || 'false',
  1943. messageTemplateCode: source.messageTemplateCode || 'false',
  1944. emailTemplateCode: source.emailTemplateCode || 'false',
  1945. appTemplateCode: source.appTemplateCode || 'false',
  1946. });
  1947. }
  1948. }
  1949. setWorkflowActiveTabKey('info');
  1950. }, [workflowNodeConfigCache, workflowWorkNodeDOList, nodeConfigs]);
  1951. // 画布点击事件(取消选择)
  1952. const onWorkflowPaneClick = useCallback(() => {
  1953. setSelectedWorkflowNode(null);
  1954. }, []);
  1955. // 实时更新节点显示(只在 workflowNodeConfig 变化时更新,避免在切换节点时覆盖)
  1956. useEffect(() => {
  1957. if (selectedWorkflowNode && workflowNodeConfig.nodeName) {
  1958. const { isolationMethod, ...restData } = selectedWorkflowNode.data || {};
  1959. // 从缓存中读取 completed 状态,保持保存状态不变
  1960. const isSaved = nodeSavedStatusCache.get(selectedWorkflowNode.id) || false;
  1961. const updatedData = {
  1962. ...restData,
  1963. label: workflowNodeConfig.nodeName,
  1964. icon: workflowNodeConfig.nodeIcon,
  1965. responsible: workflowNodeConfig.responsible,
  1966. remark: workflowNodeConfig.remark,
  1967. submitForm: workflowNodeConfig.submitForm,
  1968. isolationType: workflowNodeConfig.isolationType,
  1969. isolationPoints: workflowNodeConfig.isolationPoints,
  1970. isolationNode: workflowNodeConfig.isolationNode,
  1971. isolationNodeUuid: workflowNodeConfig.isolationNodeUuid,
  1972. lockPerson: workflowNodeConfig.lockPerson,
  1973. coLockPersons: workflowNodeConfig.coLockPersons,
  1974. notificationMethods: workflowNodeConfig.notificationMethods,
  1975. notificationPerson: workflowNodeConfig.notificationPerson,
  1976. notificationTime: workflowNodeConfig.notificationTime,
  1977. smsTemplateCode: workflowNodeConfig.smsTemplateCode,
  1978. messageTemplateCode: workflowNodeConfig.messageTemplateCode,
  1979. emailTemplateCode: workflowNodeConfig.emailTemplateCode,
  1980. appTemplateCode: workflowNodeConfig.appTemplateCode,
  1981. // 保持 completed 状态从缓存中读取,不覆盖
  1982. completed: isSaved,
  1983. };
  1984. setWorkflowNodes((nds) =>
  1985. nds.map((node) =>
  1986. node.id === selectedWorkflowNode.id
  1987. ? { ...node, data: updatedData }
  1988. : node
  1989. )
  1990. );
  1991. }
  1992. }, [workflowNodeConfig, selectedWorkflowNode?.id, setWorkflowNodes, nodeSavedStatusCache]);
  1993. // 作业管理删除处理
  1994. const handleWorkJobDelete = async (id: number) => {
  1995. Modal.confirm({
  1996. title: t('common.confirmDeleteWork'),
  1997. content: t('common.confirmDeleteWorkText'),
  1998. okText: t('common.confirmDeleteWorkButton'),
  1999. okType: 'danger',
  2000. cancelText: t('common.cancel'),
  2001. onOk: async () => {
  2002. try {
  2003. await workJobApi.deleteWorkflowWorkList([id]);
  2004. message.success(t('common.deleteWorkSuccess'));
  2005. // 删除成功后刷新列表
  2006. if (workJobList.length === 1 && workJobPagination.pageNo > 1) {
  2007. setWorkJobPagination({ ...workJobPagination, pageNo: workJobPagination.pageNo - 1 });
  2008. } else {
  2009. await getWorkJobList();
  2010. }
  2011. } catch (error: any) {
  2012. console.error('删除失败:', error);
  2013. message.error(error?.message || t('common.deleteWorkFailed'));
  2014. }
  2015. },
  2016. });
  2017. };
  2018. // 作业管理编辑处理
  2019. const handleWorkJobEdit = async (item: WorkJobVO, viewMode: boolean = false) => {
  2020. // 如果明确指定了 viewMode,设置查看模式
  2021. if (viewMode) {
  2022. setIsViewMode(true);
  2023. } else {
  2024. // 否则,如果是直接调用(不是通过 handleWorkJobView),确保是编辑模式
  2025. setIsViewMode(false);
  2026. }
  2027. try {
  2028. // 调用详情接口获取完整数据
  2029. const response = await workJobApi.selectWorkflowWorkById(item.id!);
  2030. // axios拦截器可能已经处理了数据格式,直接使用response
  2031. const detail = response as any;
  2032. // 设置作业ID,这样点击"下一步"时会调用更新接口
  2033. setWorkflowWorkId(detail.id);
  2034. // 重置表单和步骤
  2035. setWorkJobStep(0);
  2036. workJobBasicForm.resetFields();
  2037. workJobPublishForm.resetFields();
  2038. // 加载字典数据和流程模板列表
  2039. await Promise.all([
  2040. getWorkTypeDictList(),
  2041. getUrgencyLevelDictList(),
  2042. getIsolationMethodDictList(),
  2043. getWorkflowTemplateList(),
  2044. ]);
  2045. // 回显基本信息表单数据
  2046. const formData = {
  2047. workflowTemplate: detail.designId || undefined,
  2048. jobCategory: detail.type || undefined,
  2049. jobName: detail.name || '',
  2050. jobContent: detail.description || '',
  2051. urgency: detail.urgencyLevel || undefined,
  2052. };
  2053. workJobBasicForm.setFieldsValue(formData);
  2054. // 保存原始表单数据(用于判断是否有修改)
  2055. setOriginalBasicFormData(formData);
  2056. // 保存 workflowWorkNodeDOList
  2057. if (detail.workflowWorkNodeDOList && Array.isArray(detail.workflowWorkNodeDOList)) {
  2058. setWorkflowWorkNodeDOList(detail.workflowWorkNodeDOList);
  2059. } else {
  2060. setWorkflowWorkNodeDOList([]);
  2061. }
  2062. // 如果有流程设计内容,加载流程设计JSON
  2063. // 优先使用 designContent(如果存在),否则使用 designId 调用接口
  2064. // 但如果 designContent 和 workflowWorkNodeDOList 都为 null,应该清空流程
  2065. const hasDesignContent = detail.designContent && detail.designContent !== null;
  2066. const hasWorkflowNodes = detail.workflowWorkNodeDOList && Array.isArray(detail.workflowWorkNodeDOList) && detail.workflowWorkNodeDOList.length > 0;
  2067. if (hasDesignContent) {
  2068. try {
  2069. // 如果详情中直接包含流程设计内容,直接解析并设置
  2070. const jsonData = typeof detail.designContent === 'string'
  2071. ? JSON.parse(detail.designContent)
  2072. : detail.designContent;
  2073. setWorkflowJson(jsonData);
  2074. // 调用 loadWorkflowJson 的逻辑来渲染节点和连线
  2075. // 这里直接使用已有的 loadWorkflowJson 函数,但需要先设置 workflowJson
  2076. // 然后手动触发渲染逻辑
  2077. if (jsonData && jsonData.nodes && Array.isArray(jsonData.nodes) && jsonData.edges && Array.isArray(jsonData.edges)) {
  2078. // 使用 loadWorkflowJson 中的渲染逻辑
  2079. // 先创建节点映射,方便后续查找
  2080. const nodeDOMap = new Map<string, WorkflowWorkNodeDO>();
  2081. detail.workflowWorkNodeDOList?.forEach((nodeDO: WorkflowWorkNodeDO) => {
  2082. if (nodeDO.uuid) {
  2083. nodeDOMap.set(nodeDO.uuid, nodeDO);
  2084. }
  2085. });
  2086. const importedNodes: Node[] = jsonData.nodes.map((node: any) => {
  2087. const nodeData = node.data || {};
  2088. const nodeId = node.id || node.uuid;
  2089. const nodeDO = nodeDOMap.get(nodeId);
  2090. // 优先使用 nodeDO 中的 position,否则使用 JSON 中的 position
  2091. let nodePosition = { x: 0, y: 0 };
  2092. if (nodeDO?.position) {
  2093. try {
  2094. nodePosition = typeof nodeDO.position === 'string' ? JSON.parse(nodeDO.position) : nodeDO.position;
  2095. } catch (e) {
  2096. console.error('解析节点position失败:', e);
  2097. nodePosition = node.position || { x: 0, y: 0 };
  2098. }
  2099. } else {
  2100. nodePosition = node.position || { x: 0, y: 0 };
  2101. }
  2102. // 如果 nodeDO 中有 data,优先使用 nodeDO 中的数据
  2103. let finalNodeData = nodeData;
  2104. if (nodeDO?.data) {
  2105. try {
  2106. const nodeDOData = typeof nodeDO.data === 'string' ? JSON.parse(nodeDO.data) : nodeDO.data;
  2107. finalNodeData = { ...nodeData, ...nodeDOData };
  2108. } catch (e) {
  2109. console.error('解析节点data失败:', e);
  2110. }
  2111. }
  2112. return {
  2113. id: nodeId,
  2114. type: node.type || nodeDO?.type || 'createJob',
  2115. position: nodePosition,
  2116. data: (() => {
  2117. const { isolationMethod, ...restFinalNodeData } = finalNodeData || {};
  2118. return {
  2119. ...restFinalNodeData,
  2120. label: nodeDO?.nodeName || finalNodeData.label || node.label || nodeConfigs.find(c => c.type === (node.type || finalNodeData.type))?.label || '节点',
  2121. type: node.type || nodeDO?.type || finalNodeData.type || 'createJob',
  2122. // 标记节点是否已完成配置(如果 nodeDO 存在且有 id,说明已经保存过)
  2123. completed: !!nodeDO?.id,
  2124. };
  2125. })(),
  2126. };
  2127. });
  2128. const importedEdges: Edge[] = jsonData.edges.map((edge: any) => {
  2129. let sourceHandle = edge.sourceHandle;
  2130. let targetHandle = edge.targetHandle;
  2131. if (!sourceHandle && edge.id) {
  2132. const sourceMatch = edge.id.match(/right-source|left-source|top-source|bottom-source/);
  2133. if (sourceMatch) {
  2134. sourceHandle = sourceMatch[0].replace('-source', '');
  2135. } else {
  2136. sourceHandle = 'right-source';
  2137. }
  2138. } else if (!sourceHandle) {
  2139. sourceHandle = 'right-source';
  2140. }
  2141. if (!targetHandle && edge.id) {
  2142. const targetMatch = edge.id.match(/right-target|left-target|top-target|bottom-target/);
  2143. if (targetMatch) {
  2144. targetHandle = targetMatch[0].replace('-target', '');
  2145. } else {
  2146. targetHandle = 'left-target';
  2147. }
  2148. } else if (!targetHandle) {
  2149. targetHandle = 'left-target';
  2150. }
  2151. return {
  2152. id: edge.id || `${edge.source}-${edge.target}`,
  2153. source: edge.source,
  2154. target: edge.target,
  2155. sourceHandle: sourceHandle,
  2156. targetHandle: targetHandle,
  2157. type: 'straight',
  2158. style: { strokeWidth: 2, stroke: '#000000' },
  2159. markerStart: {
  2160. type: 'arrowclosed',
  2161. color: '#000000',
  2162. },
  2163. };
  2164. });
  2165. setWorkflowNodes(importedNodes);
  2166. setWorkflowEdges(importedEdges);
  2167. // 清空之前的缓存和完成状态
  2168. setWorkflowNodeConfigCache(new Map());
  2169. setCompletedNodeIds(new Set());
  2170. // 清除节点保存状态缓存
  2171. setNodeSavedStatusCache(new Map());
  2172. // 自动选中第一个节点
  2173. if (importedNodes.length > 0) {
  2174. setTimeout(() => {
  2175. const firstNode = importedNodes[0];
  2176. setSelectedWorkflowNode(firstNode);
  2177. // 查找对应的 nodeDO
  2178. const firstNodeDO = nodeDOMap.get(firstNode.id);
  2179. if (firstNodeDO) {
  2180. // 如果找到 nodeDO,从 nodeDO 中读取数据
  2181. let nodeData: any = {};
  2182. if (firstNodeDO.data) {
  2183. try {
  2184. nodeData = typeof firstNodeDO.data === 'string' ? JSON.parse(firstNodeDO.data) : firstNodeDO.data;
  2185. } catch (e) {
  2186. console.error('解析节点data失败:', e);
  2187. }
  2188. }
  2189. // 从 nodeUserList 中提取上锁人和共锁人
  2190. let lockPersonId: string | number = '';
  2191. const coLockPersonIds: (string | number)[] = [];
  2192. if (firstNodeDO.nodeUserList && Array.isArray(firstNodeDO.nodeUserList)) {
  2193. firstNodeDO.nodeUserList.forEach((user: any) => {
  2194. if (user.type === 'jtlocker' && user.userId) {
  2195. lockPersonId = typeof user.userId === 'number' ? user.userId : Number(user.userId);
  2196. } else if (user.type === 'jtcolocker' && user.userId) {
  2197. coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
  2198. }
  2199. });
  2200. }
  2201. const source = nodeData || firstNode.data || {};
  2202. const config = nodeConfigs.find(c => c.type === (firstNodeDO.type || source.type));
  2203. // 解析隔离点
  2204. let isolationPoints: string[] = [];
  2205. if (firstNodeDO.isolationPoints) {
  2206. try {
  2207. isolationPoints = typeof firstNodeDO.isolationPoints === 'string'
  2208. ? JSON.parse(firstNodeDO.isolationPoints)
  2209. : (Array.isArray(firstNodeDO.isolationPoints) ? firstNodeDO.isolationPoints : []);
  2210. } catch (e) {
  2211. console.error('解析隔离点失败:', e);
  2212. isolationPoints = source.isolationPoints || [];
  2213. }
  2214. } else {
  2215. isolationPoints = source.isolationPoints || [];
  2216. }
  2217. setWorkflowNodeConfig({
  2218. nodeName: firstNodeDO.nodeName || source.label || config?.label || '',
  2219. nodeIcon: firstNodeDO.nodeIcon || source.icon || '',
  2220. responsible: firstNodeDO.workerUserId ? (typeof firstNodeDO.workerUserId === 'number' ? firstNodeDO.workerUserId : Number(firstNodeDO.workerUserId)) : (source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined),
  2221. remark: source.remark || '',
  2222. submitForm: firstNodeDO.formId ? (typeof firstNodeDO.formId === 'number' ? firstNodeDO.formId : Number(firstNodeDO.formId)) : (source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined),
  2223. isolationType: (firstNodeDO.isolationType !== null && firstNodeDO.isolationType !== undefined) ? String(firstNodeDO.isolationType) : (source.isolationType || ''),
  2224. isolationPoints: isolationPoints,
  2225. isolationNode: source.isolationNode || [],
  2226. isolationNodeUuid: firstNodeDO.isolationNodeUuid || source.isolationNodeUuid || '',
  2227. lockPerson: lockPersonId || source.lockPerson || '',
  2228. coLockPersons: coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons || []),
  2229. notificationMethods: source.notificationMethods || {
  2230. sms: false,
  2231. message: false,
  2232. email: false,
  2233. app: false,
  2234. },
  2235. notificationPerson: source.notificationPerson || '',
  2236. notificationTime: firstNodeDO.notifyTime || source.notificationTime || '',
  2237. });
  2238. } else {
  2239. // 如果没有找到 nodeDO,使用原有逻辑
  2240. const source = firstNode.data || {};
  2241. const config = nodeConfigs.find(c => c.type === source.type);
  2242. setWorkflowNodeConfig({
  2243. nodeName: source.label || config?.label || '',
  2244. nodeIcon: source.icon || '',
  2245. responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
  2246. remark: source.remark || '',
  2247. submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
  2248. isolationType: source.isolationType || '',
  2249. isolationPoints: source.isolationPoints || [],
  2250. isolationNode: source.isolationNode || [],
  2251. isolationNodeUuid: source.isolationNodeUuid || '',
  2252. lockPerson: source.lockPerson || '',
  2253. coLockPersons: source.coLockPersons || [],
  2254. notificationMethods: source.notificationMethods || {
  2255. sms: false,
  2256. message: false,
  2257. email: false,
  2258. app: false,
  2259. },
  2260. notificationPerson: source.notificationPerson || '',
  2261. notificationTime: source.notificationTime || '',
  2262. });
  2263. }
  2264. setWorkflowNodes(prev => prev.map(n => ({
  2265. ...n,
  2266. selected: n.id === firstNode.id,
  2267. })));
  2268. }, 100);
  2269. }
  2270. }
  2271. } catch (error: any) {
  2272. console.error('解析流程设计内容失败:', error);
  2273. // 如果解析失败,清空流程
  2274. setWorkflowJson(null);
  2275. setWorkflowNodes([]);
  2276. setWorkflowEdges([]);
  2277. setWorkflowNodeConfigCache(new Map());
  2278. setCompletedNodeIds(new Set());
  2279. setNodeSavedStatusCache(new Map());
  2280. }
  2281. } else if (!hasDesignContent && !hasWorkflowNodes) {
  2282. // 如果 designContent 和 workflowWorkNodeDOList 都为 null,清空流程
  2283. console.log('designContent 和 workflowWorkNodeDOList 都为 null,清空流程');
  2284. setWorkflowJson(null);
  2285. setWorkflowNodes([]);
  2286. setWorkflowEdges([]);
  2287. setWorkflowNodeConfigCache(new Map());
  2288. setCompletedNodeIds(new Set());
  2289. setNodeSavedStatusCache(new Map());
  2290. setSelectedWorkflowNode(null);
  2291. } else if (detail.designId && hasWorkflowNodes) {
  2292. // 只有在有 workflowWorkNodeDOList 数据时,才尝试通过 designId 加载模板
  2293. // 这样可以避免在编辑时,即使没有流程数据也加载模板
  2294. try {
  2295. await loadWorkflowJson(detail.designId);
  2296. } catch (error: any) {
  2297. console.error('加载流程设计失败:', error);
  2298. // 加载失败时也清空流程
  2299. setWorkflowJson(null);
  2300. setWorkflowNodes([]);
  2301. setWorkflowEdges([]);
  2302. setWorkflowNodeConfigCache(new Map());
  2303. setCompletedNodeIds(new Set());
  2304. setNodeSavedStatusCache(new Map());
  2305. }
  2306. }
  2307. // 打开多步骤弹框
  2308. setShowAddModal(true);
  2309. setEditingItem(null); // 确保不是编辑模式(使用新增的多步骤弹框)
  2310. } catch (error: any) {
  2311. console.error('获取作业详情失败:', error);
  2312. message.error(error?.message || '获取作业详情失败');
  2313. }
  2314. };
  2315. // 作业管理查看处理(跳转到作业详情页面)
  2316. const handleWorkJobView = (item: WorkJobVO) => {
  2317. // 跳转到作业详情页面
  2318. const workId = item.id;
  2319. if (workId) {
  2320. navigate(`/work-job/detail?id=${workId}`);
  2321. } else {
  2322. message.warning(t('common.workIdNotExistsForView'));
  2323. }
  2324. };
  2325. // 为查看模式加载流程设计JSON
  2326. const loadWorkflowJsonForView = async (workflowId: number) => {
  2327. try {
  2328. const response = await workflowDesignApi.selectWorkflowDesignById(workflowId);
  2329. const workflow = response as any;
  2330. // 检查 content 是否为 null 或空字符串
  2331. const hasContent = workflow.content && workflow.content !== null && workflow.content !== '';
  2332. if (hasContent) {
  2333. try {
  2334. const jsonData = typeof workflow.content === 'string'
  2335. ? JSON.parse(workflow.content)
  2336. : workflow.content;
  2337. setViewWorkflowJson(jsonData);
  2338. } catch (parseError) {
  2339. console.error('解析流程设计JSON失败:', parseError);
  2340. // 解析失败时清空
  2341. setViewWorkflowJson(null);
  2342. }
  2343. } else {
  2344. // content 为 null 或空时,清空流程
  2345. console.warn('流程设计content为空或null,清空查看模式的流程');
  2346. setViewWorkflowJson(null);
  2347. }
  2348. } catch (error: any) {
  2349. console.error('加载流程设计失败:', error);
  2350. // 加载失败时也清空
  2351. setViewWorkflowJson(null);
  2352. }
  2353. };
  2354. // 根据当前子菜单获取数据和列配置
  2355. const getTableConfig = () => {
  2356. if (subMenu === '表单管理') {
  2357. // 表单管理使用单独的过滤逻辑
  2358. let filtered = formManagementData;
  2359. if (formManagementQuery.name) {
  2360. filtered = filtered.filter(item =>
  2361. item.name.toLowerCase().includes(formManagementQuery.name.toLowerCase())
  2362. );
  2363. }
  2364. if (formManagementQuery.status) {
  2365. filtered = filtered.filter(item => item.status === formManagementQuery.status);
  2366. }
  2367. return {
  2368. data: filtered,
  2369. columns: [
  2370. { key: 'name', label: t('table.formName'), width: '20%' },
  2371. { key: 'status', label: t('table.status'), width: '10%' },
  2372. { key: 'description', label: t('table.remark'), width: '25%' },
  2373. { key: 'createTime', label: t('table.createTime'), width: '15%' },
  2374. ],
  2375. };
  2376. } else if (subMenu === '流程设计') {
  2377. // 流程设计使用单独的过滤逻辑
  2378. let filtered = processDesignData;
  2379. if (processDesignQuery.name) {
  2380. filtered = filtered.filter(item =>
  2381. item.name.toLowerCase().includes(processDesignQuery.name.toLowerCase())
  2382. );
  2383. }
  2384. return {
  2385. data: filtered,
  2386. columns: [
  2387. { key: 'name', label: t('table.name') || '名称', width: '20%' },
  2388. { key: 'designer', label: t('table.designer'), width: '10%' },
  2389. { key: 'designTime', label: t('table.designTime'), width: '15%' },
  2390. { key: 'nodeCount', label: t('table.nodeCount'), width: '10%' },
  2391. { key: 'description', label: t('table.description'), width: '25%' },
  2392. { key: 'status', label: t('table.status'), width: '10%' },
  2393. ],
  2394. };
  2395. } else if (subMenu === '流程模板') {
  2396. return {
  2397. data: templateData,
  2398. columns: [
  2399. { key: 'code', label: t('table.templateCode'), width: '10%' },
  2400. { key: 'name', label: t('table.templateName'), width: '15%' },
  2401. { key: 'type', label: t('table.templateType'), width: '10%' },
  2402. { key: 'category', label: t('table.templateCategory'), width: '10%' },
  2403. { key: 'steps', label: t('table.stepCount'), width: '8%' },
  2404. { key: 'approvalLevel', label: t('table.approvalLevel'), width: '10%' },
  2405. { key: 'creator', label: t('table.creator'), width: '8%' },
  2406. { key: 'status', label: t('table.status'), width: '8%' },
  2407. { key: 'useCount', label: t('table.useCount'), width: '9%' },
  2408. { key: 'createTime', label: t('table.createTime'), width: '12%' },
  2409. ],
  2410. };
  2411. } else if (subMenu === 'SOP管理') {
  2412. return {
  2413. data: sopData,
  2414. columns: [
  2415. { key: 'code', label: 'SOP编号', width: '10%' },
  2416. { key: 'name', label: 'SOP名称', width: '15%' },
  2417. { key: 'version', label: '版本', width: '8%' },
  2418. { key: 'category', label: '分类', width: '10%' },
  2419. { key: 'department', label: '所属部门', width: '10%' },
  2420. { key: 'effectiveDate', label: '生效日期', width: '10%' },
  2421. { key: 'reviewDate', label: '复审日期', width: '10%' },
  2422. { key: 'status', label: '状态', width: '8%' },
  2423. { key: 'reviewer', label: '审核人', width: '8%' },
  2424. { key: 'attachments', label: '附件数', width: '8%' },
  2425. ],
  2426. };
  2427. } else if (subMenu === '作业管理') {
  2428. // 作业管理使用API数据,不需要在这里返回
  2429. return {
  2430. data: [],
  2431. columns: [],
  2432. };
  2433. }
  2434. return { data: [], columns: [] };
  2435. };
  2436. const { data, columns } = getTableConfig();
  2437. // 过滤数据(表单管理和流程设计除外,因为它们有自己的搜索逻辑)
  2438. const filteredData = (subMenu === '表单管理' || subMenu === '流程设计')
  2439. ? data
  2440. : data.filter((item) =>
  2441. Object.values(item).some((value) =>
  2442. String(value).toLowerCase().includes(searchTerm.toLowerCase())
  2443. )
  2444. );
  2445. const handleDelete = async (id: number) => {
  2446. const isProcessTemplate = subMenu === '流程模板';
  2447. Modal.confirm({
  2448. title: t('common.confirmDelete'),
  2449. content: isProcessTemplate ? t('common.deleteProcessTemplate') : `${t('common.confirmDeleteText')} ${t('isolationWork.processDesign')}?${t('common.confirmDeleteWarning')}`,
  2450. okText: t('common.confirmDeleteButton'),
  2451. okType: 'danger',
  2452. cancelText: t('common.cancel'),
  2453. onOk: async () => {
  2454. try {
  2455. await workflowDesignApi.deleteWorkflowDesignList([id]);
  2456. message.success(t('common.deleteSuccess'));
  2457. // 删除成功后刷新列表
  2458. // 如果当前页只有一条数据且不是第一页,删除后跳转到上一页
  2459. if (processDesignList.length === 1 && processDesignPagination.pageNo > 1) {
  2460. setProcessDesignPagination({ ...processDesignPagination, pageNo: processDesignPagination.pageNo - 1 });
  2461. } else {
  2462. // 否则直接刷新当前页数据
  2463. await getProcessDesignList();
  2464. }
  2465. } catch (error: any) {
  2466. console.error('删除失败:', error);
  2467. message.error(error?.message || t('common.deleteFailed'));
  2468. }
  2469. },
  2470. });
  2471. };
  2472. const handleEdit = (item: TableRow | WorkflowDesignVO) => {
  2473. setEditingItem(item as TableRow);
  2474. // 设置表单初始值
  2475. form.setFieldsValue({
  2476. name: item.name || '',
  2477. description: item.description || '',
  2478. });
  2479. setShowAddModal(true);
  2480. };
  2481. const handleDesign = (item: TableRow | WorkflowDesignVO) => {
  2482. console.log('设计流程:', item);
  2483. // 跳转到流程设计页面,传递流程ID
  2484. if (item.id) {
  2485. // 记录来源菜单:用于从流程设计器返回时恢复菜单到「隔离作业 > 流程模板」
  2486. sessionStorage.setItem(
  2487. 'processDesignerSource',
  2488. JSON.stringify({ menu: 'isolationWork', subMenu: 'processTemplate' })
  2489. );
  2490. navigate(`/process-designer?id=${item.id}`);
  2491. }
  2492. };
  2493. // 获取表单字段
  2494. const getFormFields = () => {
  2495. if (subMenu === '流程模板') {
  2496. return (
  2497. <div className="grid grid-cols-2 gap-4">
  2498. <div>
  2499. <label className="block text-sm text-gray-700 mb-2">{t('common.templateCode')} *</label>
  2500. <input
  2501. type="text"
  2502. 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"
  2503. placeholder={t('common.templateCodePlaceholder')}
  2504. defaultValue={editingItem?.code || ''}
  2505. />
  2506. </div>
  2507. <div>
  2508. <label className="block text-sm text-gray-700 mb-2">{t('form.templateName')} *</label>
  2509. <input
  2510. type="text"
  2511. 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"
  2512. placeholder={t('form.templateNamePlaceholder')}
  2513. defaultValue={editingItem?.name || ''}
  2514. />
  2515. </div>
  2516. <div>
  2517. <label className="block text-sm text-gray-700 mb-2">{t('common.processType')} *</label>
  2518. <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">
  2519. <option value="">{t('common.processTypePlaceholder')}</option>
  2520. <option value="标准流程">{t('common.standardProcess')}</option>
  2521. <option value="应急流程">{t('common.emergencyProcess')}</option>
  2522. <option value="临时流程">{t('common.temporaryProcess')}</option>
  2523. </select>
  2524. </div>
  2525. <div>
  2526. <label className="block text-sm text-gray-700 mb-2">{t('common.workCategory')} *</label>
  2527. <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">
  2528. <option value="">{t('common.workCategoryPlaceholder')}</option>
  2529. <option value="高压作业">{t('common.highVoltageWork')}</option>
  2530. <option value="低压作业">{t('common.lowVoltageWork')}</option>
  2531. <option value="应急处理">{t('common.emergencyHandling')}</option>
  2532. <option value="定期维护">{t('common.regularMaintenance')}</option>
  2533. </select>
  2534. </div>
  2535. <div>
  2536. <label className="block text-sm text-gray-700 mb-2">{t('common.approvalLevel')} *</label>
  2537. <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">
  2538. <option value="">{t('common.approvalLevelPlaceholder')}</option>
  2539. <option value="一级审批">{t('common.firstLevelApproval')}</option>
  2540. <option value="二级审批">{t('common.secondLevelApproval')}</option>
  2541. <option value="特殊审批">{t('common.specialApproval')}</option>
  2542. </select>
  2543. </div>
  2544. <div>
  2545. <label className="block text-sm text-gray-700 mb-2">{t('common.stepCount')}</label>
  2546. <input
  2547. type="number"
  2548. 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"
  2549. placeholder={t('common.stepCountPlaceholder')}
  2550. defaultValue={editingItem?.steps || ''}
  2551. />
  2552. </div>
  2553. <div className="col-span-2">
  2554. <label className="block text-sm text-gray-700 mb-2">{t('common.remark')}</label>
  2555. <textarea
  2556. rows={3}
  2557. 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"
  2558. placeholder={t('common.remarkPlaceholder')}
  2559. defaultValue={editingItem?.remark || ''}
  2560. ></textarea>
  2561. </div>
  2562. </div>
  2563. );
  2564. } else if (subMenu === 'SOP管理') {
  2565. return (
  2566. <div className="grid grid-cols-2 gap-4">
  2567. <div>
  2568. <label className="block text-sm text-gray-700 mb-2">SOP编号 *</label>
  2569. <input
  2570. type="text"
  2571. 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"
  2572. placeholder="请输入SOP编号"
  2573. defaultValue={editingItem?.code || ''}
  2574. />
  2575. </div>
  2576. <div>
  2577. <label className="block text-sm text-gray-700 mb-2">SOP名称 *</label>
  2578. <input
  2579. type="text"
  2580. 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"
  2581. placeholder="请输入SOP名称"
  2582. defaultValue={editingItem?.name || ''}
  2583. />
  2584. </div>
  2585. <div>
  2586. <label className="block text-sm text-gray-700 mb-2">版本号 *</label>
  2587. <input
  2588. type="text"
  2589. 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"
  2590. placeholder="如:V1.0"
  2591. defaultValue={editingItem?.version || ''}
  2592. />
  2593. </div>
  2594. <div>
  2595. <label className="block text-sm text-gray-700 mb-2">分类 *</label>
  2596. <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">
  2597. <option value="">请选择</option>
  2598. <option value="操作规程">操作规程</option>
  2599. <option value="安全规程">安全规程</option>
  2600. <option value="管理规程">管理规程</option>
  2601. </select>
  2602. </div>
  2603. <div>
  2604. <label className="block text-sm text-gray-700 mb-2">所属部门 *</label>
  2605. <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">
  2606. <option value="">请选择</option>
  2607. <option value="技术部">技术部</option>
  2608. <option value="运维部">运维部</option>
  2609. <option value="安全部">安全部</option>
  2610. </select>
  2611. </div>
  2612. <div>
  2613. <label className="block text-sm text-gray-700 mb-2">审核人</label>
  2614. <input
  2615. type="text"
  2616. 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"
  2617. placeholder="请输入审核人"
  2618. defaultValue={editingItem?.reviewer || ''}
  2619. />
  2620. </div>
  2621. <div>
  2622. <label className="block text-sm text-gray-700 mb-2">生效日期</label>
  2623. <input
  2624. type="date"
  2625. 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"
  2626. defaultValue={editingItem?.effectiveDate || ''}
  2627. />
  2628. </div>
  2629. <div>
  2630. <label className="block text-sm text-gray-700 mb-2">复审日期</label>
  2631. <input
  2632. type="date"
  2633. 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"
  2634. defaultValue={editingItem?.reviewDate || ''}
  2635. />
  2636. </div>
  2637. <div className="col-span-2">
  2638. <label className="block text-sm text-gray-700 mb-2">备注</label>
  2639. <textarea
  2640. rows={3}
  2641. 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"
  2642. placeholder="请输入备注"
  2643. defaultValue={editingItem?.remark || ''}
  2644. ></textarea>
  2645. </div>
  2646. </div>
  2647. );
  2648. } else if (subMenu === '作业管理') {
  2649. return (
  2650. <div className="grid grid-cols-2 gap-4">
  2651. <div>
  2652. <label className="block text-sm text-gray-700 mb-2">作业编号 *</label>
  2653. <input
  2654. type="text"
  2655. 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"
  2656. placeholder="系统自动生成"
  2657. defaultValue={editingItem?.code || ''}
  2658. disabled
  2659. />
  2660. </div>
  2661. <div>
  2662. <label className="block text-sm text-gray-700 mb-2">作业名称 *</label>
  2663. <input
  2664. type="text"
  2665. 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"
  2666. placeholder="请输入作业名称"
  2667. defaultValue={editingItem?.name || ''}
  2668. />
  2669. </div>
  2670. <div>
  2671. <label className="block text-sm text-gray-700 mb-2">作业类型 *</label>
  2672. <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">
  2673. <option value="">请选择</option>
  2674. <option value="定期维护">定期维护</option>
  2675. <option value="应急处理">应急处理</option>
  2676. <option value="故障检修">故障检修</option>
  2677. </select>
  2678. </div>
  2679. <div>
  2680. <label className="block text-sm text-gray-700 mb-2">流程模板 *</label>
  2681. <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">
  2682. <option value="">请选择</option>
  2683. <option value="高压设备隔离流程">高压设备隔离流程</option>
  2684. <option value="低压配电柜隔离流程">低压配电柜隔离流程</option>
  2685. <option value="紧急抢修隔离流程">紧急抢修隔离流程</option>
  2686. </select>
  2687. </div>
  2688. <div>
  2689. <label className="block text-sm text-gray-700 mb-2">作业位置 *</label>
  2690. <input
  2691. type="text"
  2692. 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"
  2693. placeholder="请输入作业位置"
  2694. defaultValue={editingItem?.location || ''}
  2695. />
  2696. </div>
  2697. <div>
  2698. <label className="block text-sm text-gray-700 mb-2">设备名称 *</label>
  2699. <input
  2700. type="text"
  2701. 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"
  2702. placeholder="请输入设备名称"
  2703. defaultValue={editingItem?.equipment || ''}
  2704. />
  2705. </div>
  2706. <div>
  2707. <label className="block text-sm text-gray-700 mb-2">计划开始时间 *</label>
  2708. <input
  2709. type="datetime-local"
  2710. 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"
  2711. />
  2712. </div>
  2713. <div>
  2714. <label className="block text-sm text-gray-700 mb-2">计划结束时间 *</label>
  2715. <input
  2716. type="datetime-local"
  2717. 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"
  2718. />
  2719. </div>
  2720. <div>
  2721. <label className="block text-sm text-gray-700 mb-2">优先级</label>
  2722. <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">
  2723. <option value="普通">普通</option>
  2724. <option value="重要">重要</option>
  2725. <option value="紧急">紧急</option>
  2726. </select>
  2727. </div>
  2728. <div>
  2729. <label className="block text-sm text-gray-700 mb-2">审批人</label>
  2730. <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">
  2731. <option value="">请选择</option>
  2732. <option value="张三">张三</option>
  2733. <option value="李四">李四</option>
  2734. <option value="王五">王五</option>
  2735. </select>
  2736. </div>
  2737. <div className="col-span-2">
  2738. <label className="block text-sm text-gray-700 mb-2">作业说明</label>
  2739. <textarea
  2740. rows={3}
  2741. 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"
  2742. placeholder="请输入作业说明"
  2743. defaultValue={editingItem?.remark || ''}
  2744. ></textarea>
  2745. </div>
  2746. </div>
  2747. );
  2748. }
  2749. return null;
  2750. };
  2751. // 表单管理表格列配置
  2752. const formManagementColumns: ColumnsType<TableRow> = useMemo(() => [
  2753. {
  2754. title: t('table.formId'),
  2755. width: '5%',
  2756. render: (_: any, __: TableRow, index: number) => index + 1,
  2757. },
  2758. {
  2759. title: t('table.formName'),
  2760. dataIndex: 'name',
  2761. width: '20%',
  2762. },
  2763. {
  2764. title: t('table.status'),
  2765. dataIndex: 'status',
  2766. width: '10%',
  2767. render: (status: string) => (
  2768. <span
  2769. className={`inline-flex px-3 py-1 rounded-lg text-xs ${
  2770. status === '启用' || status === t('common.enabled')
  2771. ? 'bg-green-100 text-green-700'
  2772. : 'bg-gray-100 text-gray-700'
  2773. }`}
  2774. >
  2775. {status === '启用' ? t('common.enabled') : status === '停用' ? t('common.disabled') : status}
  2776. </span>
  2777. ),
  2778. },
  2779. {
  2780. title: t('table.remark'),
  2781. dataIndex: 'description',
  2782. width: '25%',
  2783. ellipsis: true,
  2784. },
  2785. {
  2786. title: t('table.createTime'),
  2787. dataIndex: 'createTime',
  2788. width: '15%',
  2789. },
  2790. {
  2791. title: t('table.operation'),
  2792. width: '15%',
  2793. align: 'center',
  2794. render: (_: any, record: TableRow) => (
  2795. <div className="flex items-center justify-center gap-2">
  2796. <UIButton
  2797. variant="ghost"
  2798. size="sm"
  2799. onClick={() => handleEdit(record)}
  2800. className="h-8 px-2 transition-colors hover:underline"
  2801. style={{ color: '#000000' }}
  2802. onMouseEnter={(e) => {
  2803. e.currentTarget.style.color = '#1677ff';
  2804. e.currentTarget.style.textDecoration = 'underline';
  2805. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  2806. }}
  2807. onMouseLeave={(e) => {
  2808. e.currentTarget.style.color = '#000000';
  2809. e.currentTarget.style.textDecoration = 'none';
  2810. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  2811. }}
  2812. >
  2813. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  2814. <span className="ml-1">{t('common.edit')}</span>
  2815. </UIButton>
  2816. <UIButton
  2817. variant="ghost"
  2818. size="sm"
  2819. onClick={() => handleDelete(record.id)}
  2820. className="h-8 px-2 transition-colors hover:underline"
  2821. style={{ color: '#000000' }}
  2822. onMouseEnter={(e) => {
  2823. e.currentTarget.style.color = '#1677ff';
  2824. e.currentTarget.style.textDecoration = 'underline';
  2825. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  2826. }}
  2827. onMouseLeave={(e) => {
  2828. e.currentTarget.style.color = '#000000';
  2829. e.currentTarget.style.textDecoration = 'none';
  2830. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  2831. }}
  2832. >
  2833. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  2834. <span className="ml-1">{t('common.delete')}</span>
  2835. </UIButton>
  2836. </div>
  2837. ),
  2838. },
  2839. ], [t, i18n.language]);
  2840. // 流程设计表格列配置
  2841. const processDesignColumns: ColumnsType<WorkflowDesignVO> = useMemo(() => [
  2842. {
  2843. title: t('table.processDesignId'),
  2844. width: '5%',
  2845. render: (_: any, __: WorkflowDesignVO, index: number) =>
  2846. (processDesignPagination.pageNo - 1) * processDesignPagination.pageSize + index + 1,
  2847. },
  2848. {
  2849. title: t('table.name') || '名称',
  2850. dataIndex: 'name',
  2851. width: '15%',
  2852. },
  2853. {
  2854. title: t('table.nodeCount'),
  2855. dataIndex: 'nodeCount',
  2856. width: '10%',
  2857. render: (count: number) => count ?? 0,
  2858. },
  2859. {
  2860. title: t('table.status'),
  2861. dataIndex: 'status',
  2862. width: '10%',
  2863. align: 'center',
  2864. render: (status: number, record: WorkflowDesignVO) => {
  2865. const isChecked = status === 1;
  2866. const isUpdating = processDesignStatusUpdating[record.id!] || false;
  2867. return (
  2868. <AntdSwitch
  2869. checked={isChecked}
  2870. onChange={(checked) => {
  2871. if (!isUpdating) {
  2872. const newStatus = checked ? 1 : 0;
  2873. handleProcessDesignStatusChanged(record, newStatus);
  2874. }
  2875. }}
  2876. disabled={isUpdating}
  2877. loading={isUpdating}
  2878. style={{
  2879. ...(isChecked ? {
  2880. backgroundColor: '#52c41a',
  2881. } : {})
  2882. }}
  2883. className={isChecked ? 'ant-switch-checked-green' : ''}
  2884. />
  2885. );
  2886. },
  2887. },
  2888. {
  2889. title: t('table.createTime'),
  2890. dataIndex: 'createTime',
  2891. width: '15%',
  2892. render: (time: string | Date | number) => {
  2893. if (!time) return '-';
  2894. let date: Date;
  2895. if (typeof time === 'number') {
  2896. date = new Date(time);
  2897. } else if (typeof time === 'string') {
  2898. date = new Date(time);
  2899. } else {
  2900. date = time;
  2901. }
  2902. return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-CN', {
  2903. year: 'numeric',
  2904. month: '2-digit',
  2905. day: '2-digit',
  2906. hour: '2-digit',
  2907. minute: '2-digit',
  2908. second: '2-digit',
  2909. });
  2910. },
  2911. },
  2912. {
  2913. title: t('table.updateTime'),
  2914. dataIndex: 'updateTime',
  2915. width: '15%',
  2916. render: (time: string | Date | number) => {
  2917. if (!time) return '-';
  2918. let date: Date;
  2919. if (typeof time === 'number') {
  2920. date = new Date(time);
  2921. } else if (typeof time === 'string') {
  2922. date = new Date(time);
  2923. } else {
  2924. date = time;
  2925. }
  2926. return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-CN', {
  2927. year: 'numeric',
  2928. month: '2-digit',
  2929. day: '2-digit',
  2930. hour: '2-digit',
  2931. minute: '2-digit',
  2932. second: '2-digit',
  2933. });
  2934. },
  2935. },
  2936. {
  2937. title: t('table.description'),
  2938. dataIndex: 'description',
  2939. width: '18%',
  2940. render: (desc: string) => {
  2941. const text = desc || '-';
  2942. const maxLength = 20;
  2943. const shouldTruncate = text.length > maxLength;
  2944. const displayText = shouldTruncate ? text.slice(0, maxLength) + '...' : text;
  2945. return (
  2946. <Tooltip placement="topLeft" title={text}>
  2947. <span>{displayText}</span>
  2948. </Tooltip>
  2949. );
  2950. },
  2951. },
  2952. {
  2953. title: t('table.operation'),
  2954. width: '20%',
  2955. align: 'center',
  2956. render: (_: any, record: WorkflowDesignVO) => (
  2957. <div className="flex items-center justify-center gap-2">
  2958. <UIButton
  2959. variant="ghost"
  2960. size="sm"
  2961. onClick={() => handleDesign(record as any)}
  2962. className="h-8 px-2 transition-colors hover:underline"
  2963. style={{ color: '#000000' }}
  2964. onMouseEnter={(e) => {
  2965. e.currentTarget.style.color = '#1677ff';
  2966. e.currentTarget.style.textDecoration = 'underline';
  2967. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  2968. }}
  2969. onMouseLeave={(e) => {
  2970. e.currentTarget.style.color = '#000000';
  2971. e.currentTarget.style.textDecoration = 'none';
  2972. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  2973. }}
  2974. >
  2975. <Workflow className="w-4 h-4" style={{ color: '#000000' }} />
  2976. <span className="ml-0.5">{t('common.design')}</span>
  2977. </UIButton>
  2978. <UIButton
  2979. variant="ghost"
  2980. size="sm"
  2981. onClick={() => handleEdit(record as any)}
  2982. className="h-8 px-2 transition-colors hover:underline"
  2983. style={{ color: '#000000' }}
  2984. onMouseEnter={(e) => {
  2985. e.currentTarget.style.color = '#1677ff';
  2986. e.currentTarget.style.textDecoration = 'underline';
  2987. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  2988. }}
  2989. onMouseLeave={(e) => {
  2990. e.currentTarget.style.color = '#000000';
  2991. e.currentTarget.style.textDecoration = 'none';
  2992. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  2993. }}
  2994. >
  2995. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  2996. <span className="ml-0.5">{t('common.edit')}</span>
  2997. </UIButton>
  2998. <UIButton
  2999. variant="ghost"
  3000. size="sm"
  3001. onClick={() => handleDelete(record.id!)}
  3002. className="h-8 px-2 transition-colors hover:underline"
  3003. style={{ color: '#000000' }}
  3004. onMouseEnter={(e) => {
  3005. e.currentTarget.style.color = '#1677ff';
  3006. e.currentTarget.style.textDecoration = 'underline';
  3007. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  3008. }}
  3009. onMouseLeave={(e) => {
  3010. e.currentTarget.style.color = '#000000';
  3011. e.currentTarget.style.textDecoration = 'none';
  3012. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  3013. }}
  3014. >
  3015. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  3016. <span className="ml-0.5">{t('common.delete')}</span>
  3017. </UIButton>
  3018. </div>
  3019. ),
  3020. },
  3021. ], [t, i18n.language, processDesignPagination]);
  3022. // 获取作业状态样式
  3023. const getWorkJobStatusStyle = (status: string | number | undefined): React.CSSProperties => {
  3024. if (!status) {
  3025. return {
  3026. backgroundColor: '#e5e5e5',
  3027. color: '#333333',
  3028. };
  3029. }
  3030. // 从字典中查找状态文本
  3031. const statusItem = jobStatusDictList.find(item => String(item.value) === String(status));
  3032. const statusText = statusItem ? (statusItem.label || '') : String(status || '');
  3033. const statusTextLower = statusText.toLowerCase();
  3034. // 根据状态文本判断颜色
  3035. // 待执行、待发布:灰色 #e5e5e5
  3036. if (statusTextLower.includes('待执行') || statusTextLower.includes('待发布') ||
  3037. statusTextLower.includes('pending') || statusTextLower.includes('unreleased')) {
  3038. return {
  3039. backgroundColor: '#e5e5e5',
  3040. color: '#333333',
  3041. };
  3042. }
  3043. // 进行中:蓝色 #1677ff
  3044. if (statusTextLower.includes('进行中') || statusTextLower.includes('执行中') ||
  3045. statusTextLower.includes('running') || statusTextLower.includes('in_progress')) {
  3046. return {
  3047. backgroundColor: '#1677ff',
  3048. color: '#ffffff',
  3049. };
  3050. }
  3051. // 已完成:绿色 #0acb57
  3052. if (statusTextLower.includes('已完成') || statusTextLower.includes('执行完成') ||
  3053. statusTextLower.includes('completed') || statusTextLower.includes('完成')) {
  3054. return {
  3055. backgroundColor: '#0acb57',
  3056. color: '#ffffff',
  3057. };
  3058. }
  3059. // 如果没有匹配到,默认灰色
  3060. return {
  3061. backgroundColor: '#e5e5e5',
  3062. color: '#333333',
  3063. };
  3064. };
  3065. // 获取紧急程度图标和样式(根据字典 value 判断:0=一般,1=紧急,2=非常紧急)
  3066. const getUrgencyLevelIconAndStyle = (urgencyValue: string | number | undefined): { icon: React.ReactNode; style: React.CSSProperties } => {
  3067. if (urgencyValue === null || urgencyValue === undefined || urgencyValue === '') {
  3068. return {
  3069. icon: null,
  3070. style: {
  3071. backgroundColor: '#e5e5e5',
  3072. color: '#333333',
  3073. },
  3074. };
  3075. }
  3076. const value = Number(urgencyValue);
  3077. // 0 = 一般:使用 urgecy1.png 图标 + 黑色文字
  3078. if (value === 0) {
  3079. return {
  3080. icon: (
  3081. <img
  3082. src={urgecy1Icon}
  3083. alt="一般"
  3084. className="w-5 h-5 flex-shrink-0 mr-1.5"
  3085. style={{ objectFit: 'contain' }}
  3086. />
  3087. ),
  3088. style: {
  3089. backgroundColor: 'transparent',
  3090. color: '#000000',
  3091. },
  3092. };
  3093. }
  3094. // 1 = 紧急:使用 urgecy2.png 图标 + 橙色加粗文字
  3095. if (value === 1) {
  3096. return {
  3097. icon: (
  3098. <img
  3099. src={urgecy2Icon}
  3100. alt="紧急"
  3101. className="w-5 h-5 flex-shrink-0 mr-1.5"
  3102. style={{ objectFit: 'contain' }}
  3103. />
  3104. ),
  3105. style: {
  3106. backgroundColor: 'transparent',
  3107. color: '#fa8c16',
  3108. fontWeight: 'bold',
  3109. },
  3110. };
  3111. }
  3112. // 2 = 非常紧急:使用 urgecy3.png 图标 + 红色加粗文字
  3113. if (value === 2) {
  3114. return {
  3115. icon: (
  3116. <img
  3117. src={urgecy3Icon}
  3118. alt="非常紧急"
  3119. className="w-5 h-5 flex-shrink-0 mr-1.5"
  3120. style={{ objectFit: 'contain' }}
  3121. />
  3122. ),
  3123. style: {
  3124. backgroundColor: 'transparent',
  3125. color: '#ff4d4f',
  3126. fontWeight: 'bold',
  3127. },
  3128. };
  3129. }
  3130. // 如果没有匹配到,默认灰色
  3131. return {
  3132. icon: null,
  3133. style: {
  3134. backgroundColor: '#e5e5e5',
  3135. color: '#333333',
  3136. },
  3137. };
  3138. };
  3139. // 获取紧急程度样式(保留用于向后兼容)
  3140. const getUrgencyLevelStyle = (urgencyValue: string | number | undefined): React.CSSProperties => {
  3141. return getUrgencyLevelIconAndStyle(urgencyValue).style;
  3142. };
  3143. // 作业管理表格列配置
  3144. const workJobColumns: ColumnsType<WorkJobVO> = useMemo(() => [
  3145. {
  3146. title: t('table.workOrderNo'),
  3147. dataIndex: 'orderNo',
  3148. width: '12%',
  3149. render: (orderNo: string, record: WorkJobVO) => orderNo || record.code || '-',
  3150. },
  3151. {
  3152. title: t('table.workName'),
  3153. dataIndex: 'name',
  3154. width: '18%',
  3155. render: (name: string, record: WorkJobVO) => {
  3156. if (!name || name === '-') return <span>{name || '-'}</span>;
  3157. return (
  3158. <span
  3159. className="text-blue-600 cursor-pointer hover:text-blue-800 hover:underline"
  3160. onClick={(e) => {
  3161. e.stopPropagation();
  3162. handleWorkJobView(record);
  3163. }}
  3164. >
  3165. {name}
  3166. </span>
  3167. );
  3168. },
  3169. },
  3170. {
  3171. title: t('table.workStatus'),
  3172. dataIndex: 'status',
  3173. width: '10%',
  3174. render: (status: string | number) => {
  3175. const statusItem = jobStatusDictList.find(item => String(item.value) === String(status));
  3176. const statusText = statusItem ? (statusItem.label || '') : String(status || '-');
  3177. return (
  3178. <span
  3179. className="inline-flex px-3 py-1 rounded-lg text-xs"
  3180. style={getWorkJobStatusStyle(status)}
  3181. >
  3182. {statusText}
  3183. </span>
  3184. );
  3185. },
  3186. },
  3187. {
  3188. title: t('table.currentTask'),
  3189. dataIndex: 'currentNodeName',
  3190. width: '12%',
  3191. render: (currentNodeName: string, record: WorkJobVO) => currentNodeName || record.currentNode || '-',
  3192. },
  3193. {
  3194. title: t('table.workContent'),
  3195. dataIndex: 'description',
  3196. width: '20%',
  3197. render: (description: string, record: WorkJobVO) => {
  3198. const content = description || record.content || '-';
  3199. if (content === '-') return content;
  3200. const maxLength = 20;
  3201. const shouldTruncate = content.length > maxLength;
  3202. const displayText = shouldTruncate ? content.slice(0, maxLength) + '...' : content;
  3203. return (
  3204. <Tooltip placement="topLeft" title={content}>
  3205. <span>{displayText}</span>
  3206. </Tooltip>
  3207. );
  3208. },
  3209. },
  3210. {
  3211. title: t('table.initiator'),
  3212. dataIndex: 'initiatorName',
  3213. width: '10%',
  3214. render: (initiatorName: string, record: WorkJobVO) => initiatorName || record.initiator || '-',
  3215. },
  3216. {
  3217. title: t('table.initiationTime'),
  3218. dataIndex: 'initiationTime',
  3219. width: '16%',
  3220. render: (time: string | Date | number, record: WorkJobVO) => {
  3221. const actualTime = time || record.initiateTime;
  3222. if (!actualTime) return '-';
  3223. let date: Date;
  3224. if (typeof actualTime === 'number') {
  3225. date = new Date(actualTime);
  3226. } else if (typeof actualTime === 'string') {
  3227. date = new Date(actualTime);
  3228. } else {
  3229. date = actualTime;
  3230. }
  3231. return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-CN', {
  3232. year: 'numeric',
  3233. month: '2-digit',
  3234. day: '2-digit',
  3235. hour: '2-digit',
  3236. minute: '2-digit',
  3237. second: '2-digit',
  3238. });
  3239. },
  3240. },
  3241. {
  3242. title: t('table.urgencyLevel'),
  3243. dataIndex: 'urgencyLevel',
  3244. minWidth: '180px',
  3245. align: 'center',
  3246. render: (urgencyLevel: string | number | undefined, record: WorkJobVO) => {
  3247. const urgencyValue = urgencyLevel || record.urgency || record.urgencyLevel;
  3248. const urgencyItem = urgencyLevelDictList.find(item => String(item.value) === String(urgencyValue));
  3249. const urgencyText = urgencyItem ? (urgencyItem.label || '') : (urgencyValue ? String(urgencyValue) : '-');
  3250. if (!urgencyValue || urgencyValue === null || urgencyValue === undefined || urgencyValue === '') {
  3251. return <span>-</span>;
  3252. }
  3253. const { icon, style } = getUrgencyLevelIconAndStyle(urgencyValue);
  3254. return (
  3255. <span
  3256. className="inline-flex items-center justify-center gap-1.5"
  3257. >
  3258. {icon}
  3259. <span style={style}>{urgencyText}</span>
  3260. </span>
  3261. );
  3262. },
  3263. },
  3264. {
  3265. title: t('table.operation'),
  3266. width: '20%',
  3267. align: 'center',
  3268. fixed: 'right',
  3269. render: (_: any, record: WorkJobVO) => {
  3270. // 只有未发布状态(unreleased)才能编辑
  3271. const canEdit = String(record.status).toLowerCase() === 'unreleased';
  3272. return (
  3273. <div className="flex items-center justify-center gap-2">
  3274. <UIButton
  3275. variant="ghost"
  3276. size="sm"
  3277. onClick={(e) => {
  3278. e.stopPropagation();
  3279. e.preventDefault();
  3280. handleWorkJobView(record);
  3281. }}
  3282. className="h-8 px-2 transition-colors hover:underline"
  3283. style={{ color: '#000000' }}
  3284. onMouseEnter={(e) => {
  3285. e.currentTarget.style.color = '#1677ff';
  3286. e.currentTarget.style.textDecoration = 'underline';
  3287. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  3288. }}
  3289. onMouseLeave={(e) => {
  3290. e.currentTarget.style.color = '#000000';
  3291. e.currentTarget.style.textDecoration = 'none';
  3292. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  3293. }}
  3294. >
  3295. <Eye className="w-4 h-4" style={{ color: '#000000' }} />
  3296. <span className="ml-0.5">{t('table.view')}</span>
  3297. </UIButton>
  3298. {canEdit ? (
  3299. <UIButton
  3300. variant="ghost"
  3301. size="sm"
  3302. onClick={() => handleWorkJobEdit(record)}
  3303. className="h-8 px-2 transition-colors hover:underline"
  3304. style={{ color: '#000000' }}
  3305. onMouseEnter={(e) => {
  3306. e.currentTarget.style.color = '#1677ff';
  3307. e.currentTarget.style.textDecoration = 'underline';
  3308. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  3309. }}
  3310. onMouseLeave={(e) => {
  3311. e.currentTarget.style.color = '#000000';
  3312. e.currentTarget.style.textDecoration = 'none';
  3313. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  3314. }}
  3315. >
  3316. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  3317. <span className="ml-0.5">{t('common.edit')}</span>
  3318. </UIButton>
  3319. ) : (
  3320. <UIButton
  3321. variant="ghost"
  3322. size="sm"
  3323. disabled
  3324. className="h-8 px-2 opacity-50 cursor-not-allowed"
  3325. title={t('common.editDisabled') || '只有未发布状态才能编辑'}
  3326. >
  3327. <Edit2 className="w-4 h-4" />
  3328. <span className="ml-0.5">{t('common.edit')}</span>
  3329. </UIButton>
  3330. )}
  3331. <UIButton
  3332. variant="ghost"
  3333. size="sm"
  3334. onClick={() => handleWorkJobDelete(record.id!)}
  3335. className="h-8 px-2 transition-colors hover:underline"
  3336. style={{ color: '#000000' }}
  3337. onMouseEnter={(e) => {
  3338. e.currentTarget.style.color = '#1677ff';
  3339. e.currentTarget.style.textDecoration = 'underline';
  3340. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  3341. }}
  3342. onMouseLeave={(e) => {
  3343. e.currentTarget.style.color = '#000000';
  3344. e.currentTarget.style.textDecoration = 'none';
  3345. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  3346. }}
  3347. >
  3348. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  3349. <span className="ml-0.5">{t('common.delete')}</span>
  3350. </UIButton>
  3351. </div>
  3352. );
  3353. },
  3354. },
  3355. ], [t, i18n.language, jobStatusDictList, urgencyLevelDictList]);
  3356. // 如果是表单管理页面,使用特殊的布局
  3357. if (subMenu === '表单管理') {
  3358. // 计算分页数据
  3359. const formManagementTotal = filteredData.length;
  3360. const formManagementStartIndex = (formManagementPagination.pageNo - 1) * formManagementPagination.pageSize;
  3361. const formManagementEndIndex = formManagementStartIndex + formManagementPagination.pageSize;
  3362. const formManagementPageData = filteredData.slice(formManagementStartIndex, formManagementEndIndex);
  3363. const formManagementTotalPages = Math.ceil(formManagementTotal / formManagementPagination.pageSize) || 1;
  3364. return (
  3365. <div className="space-y-6">
  3366. {/* 搜索栏和表格容器 */}
  3367. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
  3368. {/* 搜索栏 */}
  3369. <div className="p-4 lg:p-5 border-b border-gray-200/50">
  3370. <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
  3371. {/* 搜索输入框 */}
  3372. <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
  3373. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  3374. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.formName')}:</label>
  3375. <Input
  3376. value={formManagementQuery.name}
  3377. onChange={(e) => setFormManagementQuery({ ...formManagementQuery, name: e.target.value })}
  3378. onPressEnter={handleFormManagementQuery}
  3379. placeholder={t('form.formNamePlaceholder')}
  3380. className="min-w-[150px] max-w-[200px]"
  3381. allowClear
  3382. />
  3383. </div>
  3384. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  3385. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.status')}:</label>
  3386. <Select
  3387. value={formManagementQuery.status}
  3388. onChange={(value) => setFormManagementQuery({ ...formManagementQuery, status: value })}
  3389. placeholder={t('form.statusPlaceholder') || t('common.pleaseSelect')}
  3390. className="min-w-[150px] max-w-[200px]"
  3391. allowClear
  3392. >
  3393. <Select.Option value="启用">{t('common.enabled')}</Select.Option>
  3394. <Select.Option value="停用">{t('common.disabled')}</Select.Option>
  3395. </Select>
  3396. </div>
  3397. </div>
  3398. {/* 操作按钮组 */}
  3399. <Space className="flex-shrink-0">
  3400. <Button
  3401. type="primary"
  3402. icon={<Search className="w-4 h-4" />}
  3403. onClick={handleFormManagementQuery}
  3404. >
  3405. {t('common.search')}
  3406. </Button>
  3407. <Button
  3408. icon={<RefreshCw className="w-4 h-4" />}
  3409. onClick={() => {
  3410. resetFormManagementQuery();
  3411. setFormManagementPagination({ pageNo: 1, pageSize: 10 });
  3412. }}
  3413. >
  3414. {t('common.reset')}
  3415. </Button>
  3416. <Button
  3417. type="primary"
  3418. icon={<Plus className="w-4 h-4" />}
  3419. onClick={() => {
  3420. setEditingItem(null);
  3421. setShowAddModal(true);
  3422. }}
  3423. >
  3424. {t('form.addForm')}
  3425. </Button>
  3426. </Space>
  3427. </div>
  3428. </div>
  3429. {/* 表格容器 */}
  3430. <div className="overflow-hidden min-w-0">
  3431. <AntdTable
  3432. columns={formManagementColumns}
  3433. dataSource={formManagementPageData}
  3434. rowKey="id"
  3435. pagination={false}
  3436. scroll={{ x: 'max-content' }}
  3437. />
  3438. </div>
  3439. </div>
  3440. {/* 分页 */}
  3441. {formManagementTotal > 0 && (
  3442. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  3443. <div className="flex items-center justify-between">
  3444. <div className="text-sm text-gray-600">
  3445. {t('common.total')} <span className="text-blue-600 font-medium">{formManagementTotal}</span> {t('common.records')}
  3446. </div>
  3447. <div className="flex gap-2">
  3448. <Button
  3449. onClick={() => setFormManagementPagination({ ...formManagementPagination, pageNo: formManagementPagination.pageNo - 1 })}
  3450. disabled={formManagementPagination.pageNo <= 1}
  3451. >
  3452. {t('common.prevPage')}
  3453. </Button>
  3454. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  3455. {formManagementPagination.pageNo} / {formManagementTotalPages}
  3456. </span>
  3457. <Button
  3458. onClick={() => setFormManagementPagination({ ...formManagementPagination, pageNo: formManagementPagination.pageNo + 1 })}
  3459. disabled={formManagementPagination.pageNo >= formManagementTotalPages}
  3460. >
  3461. {t('common.nextPage')}
  3462. </Button>
  3463. </div>
  3464. </div>
  3465. </div>
  3466. )}
  3467. {/* 新增/编辑弹窗 */}
  3468. {showAddModal && (
  3469. <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
  3470. <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">
  3471. <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between sticky top-0 bg-white z-10">
  3472. <h3 className="text-lg text-gray-900">
  3473. {editingItem ? t('common.editFormManagement') : t('common.addFormManagement')}
  3474. </h3>
  3475. <button
  3476. onClick={() => setShowAddModal(false)}
  3477. className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
  3478. >
  3479. <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  3480. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
  3481. </svg>
  3482. </button>
  3483. </div>
  3484. <div className="px-6 py-6">
  3485. <div className="grid grid-cols-2 gap-4">
  3486. <div>
  3487. <label className="block text-sm text-gray-700 mb-2">名称 *</label>
  3488. <input
  3489. type="text"
  3490. 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"
  3491. placeholder="请输入名称"
  3492. defaultValue={editingItem?.name || ''}
  3493. />
  3494. </div>
  3495. <div>
  3496. <label className="block text-sm text-gray-700 mb-2">创建人 *</label>
  3497. <input
  3498. type="text"
  3499. 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"
  3500. placeholder="请输入创建人"
  3501. defaultValue={editingItem?.creator || ''}
  3502. />
  3503. </div>
  3504. <div>
  3505. <label className="block text-sm text-gray-700 mb-2">版本</label>
  3506. <input
  3507. type="text"
  3508. 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"
  3509. placeholder="如:V1.0"
  3510. defaultValue={editingItem?.version || ''}
  3511. />
  3512. </div>
  3513. <div>
  3514. <label className="block text-sm text-gray-700 mb-2">状态 *</label>
  3515. <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">
  3516. <option value="">请选择</option>
  3517. <option value="启用">启用</option>
  3518. <option value="停用">停用</option>
  3519. </select>
  3520. </div>
  3521. <div className="col-span-2">
  3522. <label className="block text-sm text-gray-700 mb-2">{t('common.description')}</label>
  3523. <textarea
  3524. rows={3}
  3525. 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"
  3526. placeholder={t('common.descriptionPlaceholder')}
  3527. defaultValue={editingItem?.description || ''}
  3528. ></textarea>
  3529. </div>
  3530. </div>
  3531. </div>
  3532. <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
  3533. <button
  3534. onClick={() => setShowAddModal(false)}
  3535. 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"
  3536. >
  3537. 取消
  3538. </button>
  3539. <button
  3540. onClick={() => {
  3541. console.log('保存:', editingItem);
  3542. setShowAddModal(false);
  3543. }}
  3544. 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"
  3545. >
  3546. {editingItem ? '保存修改' : '确定'}
  3547. </button>
  3548. </div>
  3549. </div>
  3550. </div>
  3551. )}
  3552. </div>
  3553. );
  3554. }
  3555. // 如果是流程设计页面,使用特殊的布局
  3556. if (subMenu === '流程设计') {
  3557. // 使用接口返回的数据
  3558. const processDesignPageData = processDesignList;
  3559. const processDesignTotalPages = Math.ceil(processDesignTotal / processDesignPagination.pageSize) || 1;
  3560. return (
  3561. <div className="space-y-6">
  3562. {/* 搜索栏和表格容器 */}
  3563. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
  3564. {/* 搜索栏 */}
  3565. <div className="p-4 lg:p-5 border-b border-gray-200/50">
  3566. <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
  3567. {/* 搜索输入框 */}
  3568. <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
  3569. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  3570. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.processDesignName')}:</label>
  3571. <Input
  3572. value={processDesignQuery.name}
  3573. onChange={(e) => setProcessDesignQuery({ ...processDesignQuery, name: e.target.value })}
  3574. onPressEnter={handleProcessDesignQuery}
  3575. placeholder={t('form.processDesignNamePlaceholder')}
  3576. className="min-w-[150px] max-w-[200px]"
  3577. allowClear
  3578. />
  3579. </div>
  3580. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  3581. </div>
  3582. </div>
  3583. {/* 操作按钮组 */}
  3584. <Space className="flex-shrink-0">
  3585. <Button
  3586. type="primary"
  3587. icon={<Search className="w-4 h-4" />}
  3588. onClick={handleProcessDesignQuery}
  3589. >
  3590. {t('common.search')}
  3591. </Button>
  3592. <Button
  3593. icon={<RefreshCw className="w-4 h-4" />}
  3594. onClick={() => {
  3595. resetProcessDesignQuery();
  3596. }}
  3597. >
  3598. {t('common.reset')}
  3599. </Button>
  3600. <Button
  3601. type="primary"
  3602. icon={<Plus className="w-4 h-4" />}
  3603. onClick={() => {
  3604. setEditingItem(null);
  3605. form.resetFields();
  3606. setShowAddModal(true);
  3607. }}
  3608. >
  3609. {t('form.addProcess')}
  3610. </Button>
  3611. </Space>
  3612. </div>
  3613. </div>
  3614. {/* 表格容器 */}
  3615. <div className="overflow-hidden min-w-0">
  3616. <AntdTable
  3617. columns={processDesignColumns}
  3618. dataSource={processDesignPageData}
  3619. rowKey="id"
  3620. pagination={false}
  3621. scroll={{ x: 'max-content' }}
  3622. loading={processDesignLoading}
  3623. />
  3624. </div>
  3625. </div>
  3626. {/* 分页 */}
  3627. {processDesignTotal > 0 && (
  3628. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  3629. <div className="flex items-center justify-between">
  3630. <div className="text-sm text-gray-600">
  3631. {t('common.total')} <span className="text-blue-600 font-medium">{processDesignTotal}</span> {t('common.records')}
  3632. </div>
  3633. <div className="flex gap-2">
  3634. <Button
  3635. onClick={() => {
  3636. setProcessDesignPagination({ ...processDesignPagination, pageNo: processDesignPagination.pageNo - 1 });
  3637. }}
  3638. disabled={processDesignPagination.pageNo <= 1}
  3639. >
  3640. {t('common.prevPage')}
  3641. </Button>
  3642. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  3643. {processDesignPagination.pageNo} / {processDesignTotalPages}
  3644. </span>
  3645. <Button
  3646. onClick={() => {
  3647. setProcessDesignPagination({ ...processDesignPagination, pageNo: processDesignPagination.pageNo + 1 });
  3648. }}
  3649. disabled={processDesignPagination.pageNo >= processDesignTotalPages}
  3650. >
  3651. {t('common.nextPage')}
  3652. </Button>
  3653. </div>
  3654. </div>
  3655. </div>
  3656. )}
  3657. {/* 新增/编辑弹窗 */}
  3658. {subMenu === '流程设计' ? (
  3659. <Modal
  3660. title={editingItem ? t('common.editProcessDesign') : t('common.addProcessDesign')}
  3661. open={showAddModal}
  3662. onCancel={() => {
  3663. setShowAddModal(false);
  3664. setEditingItem(null);
  3665. form.resetFields();
  3666. }}
  3667. onOk={async () => {
  3668. try {
  3669. const values = await form.validateFields();
  3670. if (editingItem) {
  3671. // 编辑模式:调用更新接口,只更新name和description,content不变
  3672. await workflowDesignApi.updateWorkflowDesign({
  3673. id: editingItem.id!,
  3674. name: values.name,
  3675. description: values.description || '',
  3676. });
  3677. message.success(t('common.updateProcessSuccess'));
  3678. setShowAddModal(false);
  3679. setEditingItem(null);
  3680. form.resetFields();
  3681. getProcessDesignList();
  3682. } else {
  3683. // 新建模式:调用新建接口
  3684. const result = await workflowDesignApi.insertWorkflowDesign({
  3685. name: values.name,
  3686. description: values.description || '',
  3687. });
  3688. // API返回的是流程ID(数字),不是对象
  3689. const workflowId = typeof result === 'number' ? result : result?.id;
  3690. // 关闭新建弹窗
  3691. setShowAddModal(false);
  3692. setEditingItem(null);
  3693. form.resetFields();
  3694. getProcessDesignList();
  3695. // 显示确认框
  3696. if (workflowId) {
  3697. Modal.confirm({
  3698. title: t('common.processDesignComplete'),
  3699. okText: t('common.designNow'),
  3700. cancelText: t('common.later'),
  3701. onOk: () => {
  3702. // 直接跳转到流程设计页面,与设计按钮逻辑一致
  3703. navigate(`/process-designer?id=${workflowId}`);
  3704. },
  3705. });
  3706. } else {
  3707. // 如果没有ID,直接显示成功消息
  3708. message.success(t('common.addProcessSuccess'));
  3709. }
  3710. }
  3711. } catch (error: any) {
  3712. if (error.errorFields) {
  3713. // 表单验证错误
  3714. return;
  3715. }
  3716. console.error(editingItem ? '更新流程失败:' : '新建流程失败:', error);
  3717. message.error(error?.message || (editingItem ? t('common.updateProcessFailed') : t('common.addProcessFailed')));
  3718. }
  3719. }}
  3720. okText={t('common.confirm')}
  3721. cancelText={t('common.cancel')}
  3722. width={700}
  3723. destroyOnClose
  3724. >
  3725. <Form
  3726. form={form}
  3727. labelCol={{ span: 5 }}
  3728. wrapperCol={{ span: 19 }}
  3729. key={editingItem?.id || 'new'} // 使用key确保编辑时重新初始化表单
  3730. >
  3731. <Form.Item
  3732. label={t('common.processName')}
  3733. name="name"
  3734. rules={[{ required: true, message: t('common.processNameRequired') }]}
  3735. >
  3736. <Input placeholder={t('common.processNamePlaceholder')} />
  3737. </Form.Item>
  3738. <Form.Item
  3739. label={t('common.description')}
  3740. name="description"
  3741. style={{ marginBottom: '24px' }}
  3742. >
  3743. <Input.TextArea rows={7} placeholder={t('common.descriptionPlaceholder')} />
  3744. </Form.Item>
  3745. </Form>
  3746. </Modal>
  3747. ) : showAddModal && (
  3748. <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
  3749. <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">
  3750. <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between sticky top-0 bg-white z-10">
  3751. <h3 className="text-lg text-gray-900">
  3752. {editingItem ? t('common.editProcessDesign') : t('common.addProcessDesign')}
  3753. </h3>
  3754. <button
  3755. onClick={() => setShowAddModal(false)}
  3756. className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
  3757. >
  3758. <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  3759. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
  3760. </svg>
  3761. </button>
  3762. </div>
  3763. <div className="px-6 py-6">
  3764. {(
  3765. // 其他菜单的表单保持原样
  3766. <div className="grid grid-cols-2 gap-4">
  3767. <div>
  3768. <label className="block text-sm text-gray-700 mb-2">名称 *</label>
  3769. <input
  3770. type="text"
  3771. 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"
  3772. placeholder="请输入名称"
  3773. defaultValue={editingItem?.name || ''}
  3774. />
  3775. </div>
  3776. <div>
  3777. <label className="block text-sm text-gray-700 mb-2">设计人 *</label>
  3778. <input
  3779. type="text"
  3780. 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"
  3781. placeholder="请输入设计人"
  3782. defaultValue={editingItem?.designer || ''}
  3783. />
  3784. </div>
  3785. <div>
  3786. <label className="block text-sm text-gray-700 mb-2">任务数量</label>
  3787. <input
  3788. type="number"
  3789. 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"
  3790. placeholder="请输入任务数量"
  3791. defaultValue={editingItem?.nodeCount || ''}
  3792. />
  3793. </div>
  3794. <div>
  3795. <label className="block text-sm text-gray-700 mb-2">状态 *</label>
  3796. <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">
  3797. <option value="">请选择</option>
  3798. <option value="启用">启用</option>
  3799. <option value="停用">停用</option>
  3800. </select>
  3801. </div>
  3802. <div className="col-span-2">
  3803. <label className="block text-sm text-gray-700 mb-2">{t('common.description')}</label>
  3804. <textarea
  3805. rows={3}
  3806. 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"
  3807. placeholder={t('common.descriptionPlaceholder')}
  3808. defaultValue={editingItem?.description || ''}
  3809. ></textarea>
  3810. </div>
  3811. </div>
  3812. )}
  3813. </div>
  3814. <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
  3815. <button
  3816. onClick={() => setShowAddModal(false)}
  3817. 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"
  3818. >
  3819. 取消
  3820. </button>
  3821. <button
  3822. onClick={() => {
  3823. // 其他菜单的保存逻辑
  3824. console.log('保存:', editingItem);
  3825. setShowAddModal(false);
  3826. }}
  3827. 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"
  3828. >
  3829. {editingItem ? '保存修改' : '确定'}
  3830. </button>
  3831. </div>
  3832. </div>
  3833. </div>
  3834. )}
  3835. </div>
  3836. );
  3837. }
  3838. // 如果是作业管理页面,使用特殊的布局
  3839. if (subMenu === '作业管理') {
  3840. const workJobPageData = workJobList;
  3841. return (
  3842. <div className="space-y-6">
  3843. {/* 搜索栏和表格容器 */}
  3844. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
  3845. {/* 搜索栏 */}
  3846. <div className="p-4 lg:p-5 border-b border-gray-200/50">
  3847. <div className="flex flex-wrap items-center justify-between gap-3 lg:gap-4">
  3848. <div className="flex flex-wrap items-center gap-3 lg:gap-4">
  3849. <div className="flex items-center gap-2">
  3850. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.workName')}:</label>
  3851. <Input
  3852. value={workJobQuery.name}
  3853. onChange={(e) => setWorkJobQuery({ ...workJobQuery, name: e.target.value })}
  3854. onPressEnter={handleWorkJobQuery}
  3855. placeholder={t('form.workNamePlaceholder')}
  3856. allowClear
  3857. className="min-w-[200px] max-w-[300px]"
  3858. />
  3859. </div>
  3860. <div className="flex items-center gap-2">
  3861. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.workStatus')}:</label>
  3862. <Select
  3863. value={workJobQuery.status ?? undefined}
  3864. onChange={(value) => setWorkJobQuery({ ...workJobQuery, status: value || undefined })}
  3865. placeholder={t('form.workStatusPlaceholder')}
  3866. allowClear
  3867. className="w-32"
  3868. >
  3869. {jobStatusDictList.map(item => (
  3870. <Select.Option key={item.value} value={item.value}>
  3871. {item.label}
  3872. </Select.Option>
  3873. ))}
  3874. </Select>
  3875. </div>
  3876. </div>
  3877. <Space>
  3878. <Button
  3879. type="primary"
  3880. icon={<Search className="w-4 h-4" />}
  3881. onClick={handleWorkJobQuery}
  3882. >
  3883. {t('common.search')}
  3884. </Button>
  3885. <Button
  3886. icon={<RefreshCw className="w-4 h-4" />}
  3887. onClick={() => {
  3888. resetWorkJobQuery();
  3889. getWorkJobList();
  3890. }}
  3891. >
  3892. {t('common.reset')}
  3893. </Button>
  3894. <Button
  3895. type="primary"
  3896. icon={<Plus className="w-4 h-4" />}
  3897. onClick={() => {
  3898. setEditingItem(null);
  3899. setShowAddModal(true);
  3900. setOriginalBasicFormData(null); // 清空原始表单数据(新增模式)
  3901. // 打开弹框时加载数据
  3902. getWorkTypeDictList();
  3903. getUrgencyLevelDictList();
  3904. getIsolationMethodDictList();
  3905. getWorkflowTemplateList();
  3906. }}
  3907. >
  3908. {t('form.initiateWork')}
  3909. </Button>
  3910. </Space>
  3911. </div>
  3912. </div>
  3913. {/* 表格容器 */}
  3914. <div className="overflow-hidden min-w-0">
  3915. <AntdTable
  3916. columns={workJobColumns}
  3917. dataSource={workJobPageData}
  3918. rowKey="id"
  3919. pagination={false}
  3920. scroll={{ x: 'max-content' }}
  3921. loading={workJobLoading}
  3922. />
  3923. </div>
  3924. </div>
  3925. {/* 分页 */}
  3926. {workJobTotal > 0 && (
  3927. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  3928. <div className="flex items-center justify-between">
  3929. <div className="text-sm text-gray-600">
  3930. {t('common.total')} <span className="text-blue-600 font-medium">{workJobTotal}</span> {t('common.records')}
  3931. </div>
  3932. <div className="flex gap-2">
  3933. <Button
  3934. onClick={() => setWorkJobPagination({ ...workJobPagination, pageNo: workJobPagination.pageNo - 1 })}
  3935. disabled={workJobPagination.pageNo <= 1}
  3936. >
  3937. {t('common.prevPage')}
  3938. </Button>
  3939. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  3940. {workJobPagination.pageNo} / {Math.ceil(workJobTotal / workJobPagination.pageSize) || 1}
  3941. </span>
  3942. <Button
  3943. onClick={() => setWorkJobPagination({ ...workJobPagination, pageNo: workJobPagination.pageNo + 1 })}
  3944. disabled={workJobPagination.pageNo >= Math.ceil(workJobTotal / workJobPagination.pageSize)}
  3945. >
  3946. {t('common.nextPage')}
  3947. </Button>
  3948. </div>
  3949. </div>
  3950. </div>
  3951. )}
  3952. {/* 发起作业多步骤弹窗(新增和编辑共用) */}
  3953. {subMenu === '作业管理' && showAddModal && !editingItem ? (
  3954. <Modal
  3955. title={null}
  3956. open={showAddModal}
  3957. onCancel={() => {
  3958. setShowAddModal(false);
  3959. setWorkJobStep(0);
  3960. workJobBasicForm.resetFields();
  3961. workJobPublishForm.resetFields();
  3962. setWorkflowJson(null);
  3963. setWorkflowWorkId(null); // 清空作业ID
  3964. setIsViewMode(false); // 重置查看模式
  3965. // 清空流程管理相关状态
  3966. setWorkflowNodes([]);
  3967. setWorkflowEdges([]);
  3968. setSelectedWorkflowNode(null);
  3969. setWorkflowNodeConfigCache(new Map());
  3970. setCompletedNodeIds(new Set());
  3971. setNodeSavedStatusCache(new Map());
  3972. setWorkflowWorkNodeDOList([]); // 清空节点数据列表
  3973. setOriginalBasicFormData(null); // 清空原始表单数据
  3974. // 关闭弹框时刷新列表
  3975. getWorkJobList();
  3976. }}
  3977. footer={null}
  3978. width={1400}
  3979. destroyOnClose
  3980. styles={{
  3981. body: { padding: 0 }
  3982. }}
  3983. >
  3984. <div className="flex flex-col h-[80vh]">
  3985. {/* Tab导航 */}
  3986. <div className="px-6 py-4 bg-white">
  3987. <Tabs
  3988. activeKey={String(workJobStep)}
  3989. onChange={(key) => {
  3990. // 查看模式下可以自由切换tab
  3991. if (isViewMode) {
  3992. setWorkJobStep(Number(key));
  3993. return;
  3994. }
  3995. // 只有第一个tab完成才能进入第二个tab
  3996. if (key === '1' && workJobStep === 0) {
  3997. workJobBasicForm.validateFields().then(() => {
  3998. setWorkJobStep(1);
  3999. }).catch(() => {
  4000. message.warning(t('common.pleaseCompleteBasicInfo'));
  4001. });
  4002. } else if (key === '2' && workJobStep < 2) {
  4003. // 只有第二个tab完成才能进入第三个tab
  4004. if (workflowJson) {
  4005. setWorkJobStep(2);
  4006. } else {
  4007. message.warning(t('common.pleaseCompleteWorkflowManagement'));
  4008. }
  4009. } else {
  4010. setWorkJobStep(Number(key));
  4011. }
  4012. }}
  4013. items={[
  4014. {
  4015. key: '0',
  4016. label: (
  4017. <span className="flex items-center gap-2">
  4018. <FileText className="w-4 h-4" />
  4019. {t('common.basicInfo')}
  4020. </span>
  4021. ),
  4022. },
  4023. {
  4024. key: '1',
  4025. label: (
  4026. <span className="flex items-center gap-2">
  4027. <Workflow className="w-4 h-4" />
  4028. {t('common.workflowManagement')}
  4029. </span>
  4030. ),
  4031. disabled: isViewMode ? false : workJobStep < 1,
  4032. },
  4033. {
  4034. key: '2',
  4035. label: (
  4036. <span className="flex items-center gap-2">
  4037. <Play className="w-4 h-4" />
  4038. {t('common.publishWork')}
  4039. </span>
  4040. ),
  4041. disabled: isViewMode ? false : workJobStep < 2,
  4042. },
  4043. ]}
  4044. />
  4045. </div>
  4046. {/* Tab内容 */}
  4047. <div className="flex-1 overflow-y-auto px-6 py-4 bg-white">
  4048. {workJobStep === 0 && (
  4049. <div className="flex justify-center">
  4050. <Form
  4051. form={workJobBasicForm}
  4052. layout="vertical"
  4053. className="max-w-2xl w-full"
  4054. >
  4055. <Form.Item
  4056. label={t('common.workflowTemplate')}
  4057. name="workflowTemplate"
  4058. required
  4059. rules={[{ required: true, message: t('common.workflowTemplateRequired') }]}
  4060. help={t('common.workflowTemplateHelp')}
  4061. >
  4062. <Select
  4063. placeholder={t('common.workflowTemplatePlaceholder')}
  4064. allowClear
  4065. showSearch
  4066. disabled={isViewMode}
  4067. filterOption={(input, option) =>
  4068. (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
  4069. }
  4070. options={workflowTemplateList
  4071. .filter(item => item.status === 1) // 只显示启用的
  4072. .map(item => ({
  4073. label: item.name,
  4074. value: item.id,
  4075. }))}
  4076. onChange={(value) => {
  4077. if (value) {
  4078. // 加载流程设计JSON
  4079. loadWorkflowJson(value);
  4080. // 自动填充作业名称为流程模板名称
  4081. const selectedTemplate = workflowTemplateList.find(item => item.id === value);
  4082. if (selectedTemplate?.name) {
  4083. workJobBasicForm.setFieldsValue({ jobName: selectedTemplate.name });
  4084. }
  4085. } else {
  4086. setWorkflowJson(null);
  4087. // 清空作业名称
  4088. workJobBasicForm.setFieldsValue({ jobName: '' });
  4089. }
  4090. }}
  4091. />
  4092. </Form.Item>
  4093. <Form.Item
  4094. label={t('common.jobCategory')}
  4095. name="jobCategory"
  4096. required
  4097. rules={[{ required: true, message: t('common.jobCategoryRequired') }]}
  4098. >
  4099. <Select placeholder={t('common.jobCategoryPlaceholder')} allowClear disabled={isViewMode}>
  4100. {workTypeDictList.map((item) => (
  4101. <Select.Option key={item.id} value={item.value}>
  4102. {item.label}
  4103. </Select.Option>
  4104. ))}
  4105. </Select>
  4106. </Form.Item>
  4107. <Form.Item
  4108. label={t('common.workName')}
  4109. name="jobName"
  4110. required
  4111. rules={[{ required: true, message: t('common.workNameRequired') }]}
  4112. >
  4113. <Input placeholder={t('common.workNamePlaceholder')} disabled={isViewMode} readOnly={isViewMode} />
  4114. </Form.Item>
  4115. <Form.Item
  4116. label={t('common.workContent')}
  4117. name="jobContent"
  4118. required
  4119. rules={[{ required: true, message: t('common.workContentPlaceholder') }]}
  4120. >
  4121. <Input.TextArea
  4122. rows={4}
  4123. placeholder={t('common.workContentPlaceholder')}
  4124. showCount
  4125. maxLength={500}
  4126. disabled={isViewMode}
  4127. readOnly={isViewMode}
  4128. />
  4129. </Form.Item>
  4130. <Form.Item
  4131. label={t('common.urgencyLevel')}
  4132. name="urgency"
  4133. >
  4134. <Radio.Group disabled={isViewMode}>
  4135. {urgencyLevelDictList.length > 0 ? (
  4136. urgencyLevelDictList.map((item) => (
  4137. <Radio key={item.id} value={item.value}>
  4138. {item.label}
  4139. </Radio>
  4140. ))
  4141. ) : (
  4142. <span className="text-gray-400 text-sm">{t('common.loading')}</span>
  4143. )}
  4144. </Radio.Group>
  4145. </Form.Item>
  4146. </Form>
  4147. </div>
  4148. )}
  4149. {workJobStep === 1 && (
  4150. <div className="flex gap-4" style={{ height: 'calc(80vh - 250px)' }}>
  4151. {/* 左侧:流程设计渲染区域(面积大一些) */}
  4152. <div className="flex-1 border border-gray-200 rounded-lg overflow-hidden bg-gray-50" style={{ minWidth: 0 }}>
  4153. {workflowNodes.length > 0 ? (
  4154. <div className="h-full" ref={workflowReactFlowWrapper}>
  4155. <style>{`
  4156. .react-flow__arrowhead {
  4157. fill: #000000 !important;
  4158. }
  4159. .react-flow__edge-path {
  4160. stroke: #000000 !important;
  4161. }
  4162. .react-flow__edge marker path {
  4163. fill: #000000 !important;
  4164. stroke: #000000 !important;
  4165. }
  4166. .react-flow__edge path {
  4167. stroke: #000000 !important;
  4168. }
  4169. svg marker path {
  4170. fill: #000000 !important;
  4171. stroke: #000000 !important;
  4172. }
  4173. svg marker polygon {
  4174. fill: #000000 !important;
  4175. stroke: #000000 !important;
  4176. }
  4177. .react-flow__edge .react-flow__edge-path {
  4178. stroke: #000000 !important;
  4179. }
  4180. .react-flow__edge svg path {
  4181. stroke: #000000 !important;
  4182. }
  4183. .react-flow__edge svg marker polygon {
  4184. fill: #000000 !important;
  4185. stroke: #000000 !important;
  4186. }
  4187. .react-flow__edge svg marker path {
  4188. fill: #000000 !important;
  4189. stroke: #000000 !important;
  4190. }
  4191. .react-flow__edge svg marker {
  4192. fill: #000000 !important;
  4193. }
  4194. svg defs marker path {
  4195. fill: #000000 !important;
  4196. stroke: #000000 !important;
  4197. }
  4198. svg defs marker polygon {
  4199. fill: #000000 !important;
  4200. stroke: #000000 !important;
  4201. }
  4202. .react-flow__edge svg defs marker path {
  4203. fill: #000000 !important;
  4204. stroke: #000000 !important;
  4205. }
  4206. .react-flow__edge svg defs marker polygon {
  4207. fill: #000000 !important;
  4208. stroke: #000000 !important;
  4209. }
  4210. `}</style>
  4211. <ReactFlow
  4212. nodes={workflowNodes}
  4213. edges={workflowEdges}
  4214. onNodesChange={isViewMode ? undefined : onWorkflowNodesChange}
  4215. onEdgesChange={isViewMode ? undefined : onWorkflowEdgesChange}
  4216. onNodeClick={isViewMode ? undefined : onWorkflowNodeClick}
  4217. onPaneClick={isViewMode ? undefined : onWorkflowPaneClick}
  4218. nodesDraggable={!isViewMode}
  4219. nodesConnectable={!isViewMode}
  4220. elementsSelectable={!isViewMode}
  4221. nodeTypes={nodeTypes}
  4222. edgeTypes={edgeTypes}
  4223. fitView
  4224. onInit={(instance) => {
  4225. setWorkflowReactFlowInstance(instance);
  4226. // 初始化后自动适应视图
  4227. setTimeout(() => {
  4228. instance.fitView({ padding: 0.2 });
  4229. }, 100);
  4230. }}
  4231. connectionMode={ConnectionMode.Loose}
  4232. defaultEdgeOptions={{
  4233. style: { strokeWidth: 2, stroke: '#000000' },
  4234. type: 'straight',
  4235. markerStart: {
  4236. type: 'arrowclosed',
  4237. color: '#000000',
  4238. },
  4239. }}
  4240. edgesUpdatable={!isViewMode}
  4241. >
  4242. <Controls className="!bg-white !border !border-gray-200 !rounded-lg !shadow-md" />
  4243. <Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#e5e7eb" />
  4244. </ReactFlow>
  4245. </div>
  4246. ) : (
  4247. <div className="flex items-center justify-center h-full text-gray-400">
  4248. {workflowJson ? '正在加载流程设计...' : '请先在基本信息中选择流程模板'}
  4249. </div>
  4250. )}
  4251. </div>
  4252. {/* 右侧:配置面板 */}
  4253. <div className="w-80 border border-gray-200 rounded-lg bg-white overflow-y-auto flex-shrink-0">
  4254. {selectedWorkflowNode ? (
  4255. <div className="h-full flex flex-col">
  4256. {/* 头部 */}
  4257. <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">
  4258. <h3 className="text-sm font-medium text-gray-900">
  4259. {workflowNodeConfig.nodeName || selectedWorkflowNode.data?.label || '节点'} 设置
  4260. </h3>
  4261. <button
  4262. onClick={() => setSelectedWorkflowNode(null)}
  4263. className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
  4264. disabled={isViewMode}
  4265. >
  4266. <CloseOutlined className="text-gray-500" />
  4267. </button>
  4268. </div>
  4269. {/* 内容 */}
  4270. <div className="flex-1 p-4 overflow-y-auto space-y-6">
  4271. {/* 节点信息 */}
  4272. <div>
  4273. <h4 className="flex items-center gap-3 text-base font-semibold text-gray-900 mb-4">
  4274. <span className="w-1 h-5 bg-blue-500 rounded-full flex-shrink-0" style={{ minWidth: '4px', minHeight: '20px' }}></span>
  4275. 节点信息
  4276. </h4>
  4277. <div className="space-y-5">
  4278. {/* 节点名称 */}
  4279. <div>
  4280. <label className="block text-sm font-medium text-gray-700 mb-2">
  4281. 节点名称 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
  4282. </label>
  4283. <Input
  4284. value={workflowNodeConfig.nodeName || ''}
  4285. onChange={(e) =>
  4286. setWorkflowNodeConfig({ ...workflowNodeConfig, nodeName: e.target.value })
  4287. }
  4288. placeholder="请输入节点名称"
  4289. className="rounded-lg border-gray-200"
  4290. disabled={isViewMode}
  4291. />
  4292. </div>
  4293. {/* 负责人 */}
  4294. {selectedWorkflowNode.data?.type !== 'createJob' && selectedWorkflowNode.data?.type !== 'isolation' && selectedWorkflowNode.data?.type !== 'releaseIsolation' && (
  4295. <div>
  4296. <label className="block text-sm font-medium text-gray-700 mb-2">
  4297. 负责人 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
  4298. </label>
  4299. <Select
  4300. value={workflowNodeConfig.responsible || undefined}
  4301. onChange={(value) =>
  4302. setWorkflowNodeConfig({ ...workflowNodeConfig, responsible: value || undefined })
  4303. }
  4304. placeholder="请选择负责人"
  4305. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
  4306. allowClear
  4307. disabled={isViewMode}
  4308. >
  4309. {workflowDrawerUsers.map(user => (
  4310. <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
  4311. ))}
  4312. </Select>
  4313. <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">
  4314. 对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择。
  4315. </p>
  4316. </div>
  4317. )}
  4318. {/* 备注 */}
  4319. {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' && (
  4320. <div>
  4321. <label className="block text-sm font-medium text-gray-700 mb-2">
  4322. 备注
  4323. </label>
  4324. <Input.TextArea
  4325. value={workflowNodeConfig.remark || ''}
  4326. onChange={(e) =>
  4327. setWorkflowNodeConfig({ ...workflowNodeConfig, remark: e.target.value })
  4328. }
  4329. placeholder="请输入备注"
  4330. rows={3}
  4331. className="rounded-lg border-gray-200"
  4332. disabled={isViewMode}
  4333. />
  4334. </div>
  4335. )}
  4336. </div>
  4337. </div>
  4338. {/* 提交表单 - 创建作业节点不显示 */}
  4339. {selectedWorkflowNode.data?.type !== 'createJob' && (
  4340. <div>
  4341. <h4 className="flex items-center gap-3 text-base font-semibold text-gray-900 mb-4">
  4342. <span className="w-1 h-5 bg-blue-500 rounded-full flex-shrink-0" style={{ minWidth: '4px', minHeight: '20px' }}></span>
  4343. 提交表单
  4344. </h4>
  4345. <div className="space-y-5">
  4346. <div>
  4347. <label className="block text-sm font-medium text-gray-700 mb-2">
  4348. 业务表单
  4349. {/* 只有确认节点、审核节点、录入信息节点显示必填标记 */}
  4350. {['confirm', 'review', 'inputInfo'].includes(selectedWorkflowNode.data?.type || '') && (
  4351. <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
  4352. )}
  4353. </label>
  4354. <Select
  4355. value={workflowNodeConfig.submitForm || undefined}
  4356. onChange={(value) =>
  4357. setWorkflowNodeConfig({ ...workflowNodeConfig, submitForm: value || undefined })
  4358. }
  4359. placeholder="请选择提交表单"
  4360. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
  4361. allowClear
  4362. disabled={isViewMode}
  4363. >
  4364. {workflowFormList.map(form => (
  4365. <Select.Option key={form.id} value={form.id}>{form.name}</Select.Option>
  4366. ))}
  4367. </Select>
  4368. </div>
  4369. {/* 隔离/方案 和 解除隔离 节点特有的字段 */}
  4370. {(selectedWorkflowNode.data?.type === 'isolation' || selectedWorkflowNode.data?.type === 'releaseIsolation') && (
  4371. <>
  4372. {/* 解除隔离节点:选择隔离节点 */}
  4373. {selectedWorkflowNode.data?.type === 'releaseIsolation' && (
  4374. <div>
  4375. <label className="block text-sm font-medium text-gray-700 mb-2">
  4376. 选择隔离节点 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
  4377. </label>
  4378. <Select
  4379. value={workflowNodeConfig.isolationNodeUuid || undefined}
  4380. disabled={isViewMode}
  4381. onChange={(value) => {
  4382. setWorkflowNodeConfig((prev) => {
  4383. if (!value) {
  4384. return { ...prev, isolationNodeUuid: '' };
  4385. }
  4386. // 从 workflowWorkNodeDOList 中查找对应的隔离节点数据
  4387. const isolationNodeDO = workflowWorkNodeDOList.find(item => item.uuid === value && item.type === 'isolation');
  4388. if (isolationNodeDO) {
  4389. // 解析隔离节点的 data 字段
  4390. let isolationNodeData: any = {};
  4391. if (isolationNodeDO.data) {
  4392. try {
  4393. isolationNodeData = typeof isolationNodeDO.data === 'string'
  4394. ? JSON.parse(isolationNodeDO.data)
  4395. : isolationNodeDO.data;
  4396. } catch (e) {
  4397. console.error('解析隔离节点data失败:', e);
  4398. }
  4399. }
  4400. // 解析隔离点
  4401. let isolationPoints: string[] = [];
  4402. if (isolationNodeDO.isolationPoints) {
  4403. try {
  4404. isolationPoints = typeof isolationNodeDO.isolationPoints === 'string'
  4405. ? JSON.parse(isolationNodeDO.isolationPoints)
  4406. : (Array.isArray(isolationNodeDO.isolationPoints) ? isolationNodeDO.isolationPoints : []);
  4407. } catch (e) {
  4408. console.error('解析隔离点失败:', e);
  4409. isolationPoints = isolationNodeData.isolationPoints || [];
  4410. }
  4411. } else {
  4412. isolationPoints = isolationNodeData.isolationPoints || [];
  4413. }
  4414. // 从 nodeUserList 中提取上锁人和共锁人
  4415. let lockPersonId: number | undefined = undefined;
  4416. const coLockPersonIds: (number | string)[] = [];
  4417. if (isolationNodeDO.nodeUserList && Array.isArray(isolationNodeDO.nodeUserList)) {
  4418. isolationNodeDO.nodeUserList.forEach((user: any) => {
  4419. if (user.type === 'jtlocker' && user.userId) {
  4420. lockPersonId = typeof user.userId === 'number' ? user.userId : Number(user.userId);
  4421. } else if (user.type === 'jtcolocker' && user.userId) {
  4422. coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
  4423. }
  4424. });
  4425. }
  4426. // 复制隔离节点的所有数据到解除隔离节点
  4427. return {
  4428. ...prev,
  4429. isolationNodeUuid: value || '',
  4430. // 复制隔离方式
  4431. isolationType: (isolationNodeDO.isolationType !== null && isolationNodeDO.isolationType !== undefined)
  4432. ? String(isolationNodeDO.isolationType)
  4433. : (isolationNodeData.isolationType || ''),
  4434. // 复制隔离点
  4435. isolationPoints: isolationPoints,
  4436. // 复制负责人(如果隔离方式是盲板或拆除)
  4437. responsible: isolationNodeDO.workerUserId
  4438. ? (typeof isolationNodeDO.workerUserId === 'number' ? isolationNodeDO.workerUserId : Number(isolationNodeDO.workerUserId))
  4439. : (isolationNodeData.workerUserId ? (typeof isolationNodeData.workerUserId === 'number' ? isolationNodeData.workerUserId : Number(isolationNodeData.workerUserId)) : undefined),
  4440. // 复制上锁人
  4441. lockPerson: lockPersonId || (isolationNodeData.lockPerson ? (typeof isolationNodeData.lockPerson === 'number' ? isolationNodeData.lockPerson : Number(isolationNodeData.lockPerson)) : undefined),
  4442. // 复制共锁人
  4443. coLockPersons: coLockPersonIds.length > 0
  4444. ? coLockPersonIds
  4445. : (isolationNodeData.coLockPersons && Array.isArray(isolationNodeData.coLockPersons)
  4446. ? isolationNodeData.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id))
  4447. : []),
  4448. };
  4449. }
  4450. // 如果没找到 nodeDO,尝试从 workflowNodes 中获取
  4451. const target = workflowNodes.find(n => n.id === value && n.data?.type === 'isolation');
  4452. if (target) {
  4453. const source = target.data || {};
  4454. return {
  4455. ...prev,
  4456. isolationNodeUuid: value || '',
  4457. isolationType: source.isolationType || '',
  4458. isolationPoints: source.isolationPoints || [],
  4459. isolationNode: Array.isArray(source.isolationNode)
  4460. ? source.isolationNode
  4461. : (source.isolationNode ? [source.isolationNode] : []),
  4462. responsible: source.workerUserId ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId)) : undefined,
  4463. lockPerson: source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined,
  4464. coLockPersons: source.coLockPersons && Array.isArray(source.coLockPersons)
  4465. ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id))
  4466. : [],
  4467. };
  4468. }
  4469. return { ...prev, isolationNodeUuid: value || '' };
  4470. });
  4471. }}
  4472. placeholder="请选择隔离节点"
  4473. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
  4474. allowClear
  4475. >
  4476. {(() => {
  4477. // 找到所有与当前"解除隔离"节点连线的"隔离/方案"节点
  4478. // 连线方向:隔离节点(source) -> 解除隔离节点(target)
  4479. const currentNodeId = selectedWorkflowNode?.id;
  4480. // 找到所有以当前节点为 target 的 edge(隔离节点指向解除隔离节点)
  4481. const incomingEdges = workflowEdges.filter(edge =>
  4482. String(edge.target) === String(currentNodeId)
  4483. );
  4484. // 也检查反向连线(解除隔离节点指向隔离节点,虽然不常见但兼容)
  4485. const outgoingEdges = workflowEdges.filter(edge =>
  4486. String(edge.source) === String(currentNodeId)
  4487. );
  4488. // 收集所有已连线的隔离节点ID
  4489. const connectedIsolationNodeIds = new Set<string>();
  4490. incomingEdges.forEach(edge => {
  4491. connectedIsolationNodeIds.add(String(edge.source));
  4492. });
  4493. outgoingEdges.forEach(edge => {
  4494. connectedIsolationNodeIds.add(String(edge.target));
  4495. });
  4496. // 获取所有隔离节点
  4497. const isolationNodes = workflowNodes.filter(n => n.data?.type === 'isolation');
  4498. // 如果有连线,只显示已连线的隔离节点;否则显示所有隔离节点(兼容旧数据)
  4499. const nodesToShow = connectedIsolationNodeIds.size > 0
  4500. ? isolationNodes.filter(n => connectedIsolationNodeIds.has(String(n.id)))
  4501. : isolationNodes;
  4502. return nodesToShow.map(node => {
  4503. // 从 workflowWorkNodeDOList 中查找对应的节点数据,获取 nodeName
  4504. const nodeDO = workflowWorkNodeDOList.find(item => item.uuid === node.id && item.type === 'isolation');
  4505. const displayName = nodeDO?.nodeName || node.data?.label || nodeConfigs.find(c => c.type === 'isolation')?.label || '隔离/方案';
  4506. return (
  4507. <Select.Option key={node.id} value={node.id}>
  4508. {displayName}
  4509. </Select.Option>
  4510. );
  4511. });
  4512. })()}
  4513. </Select>
  4514. </div>
  4515. )}
  4516. {/* 隔离方式 */}
  4517. <div>
  4518. <label className="block text-sm font-medium text-gray-700 mb-2">
  4519. 隔离方式 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
  4520. </label>
  4521. <Select
  4522. value={workflowNodeConfig.isolationType || undefined}
  4523. onChange={(value) =>
  4524. setWorkflowNodeConfig({ ...workflowNodeConfig, isolationType: value || '' })
  4525. }
  4526. placeholder="请选择隔离方式"
  4527. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
  4528. allowClear
  4529. disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
  4530. >
  4531. {isolationTypeDictList.map((item) => (
  4532. <Select.Option key={item.id} value={item.value}>
  4533. {item.label}
  4534. </Select.Option>
  4535. ))}
  4536. </Select>
  4537. </div>
  4538. {/* 隔离点选择(可多选) */}
  4539. {(selectedWorkflowNode.data?.type === 'isolation' || selectedWorkflowNode.data?.type === 'releaseIsolation') && (
  4540. <div>
  4541. <label className="block text-sm font-medium text-gray-700 mb-2">
  4542. 隔离点选择(可多选) <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
  4543. </label>
  4544. <Select
  4545. mode="multiple"
  4546. value={workflowNodeConfig.isolationPoints}
  4547. onChange={(value) =>
  4548. setWorkflowNodeConfig({ ...workflowNodeConfig, isolationPoints: value })
  4549. }
  4550. placeholder="请选择隔离点"
  4551. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!min-h-10"
  4552. allowClear
  4553. disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
  4554. >
  4555. {workflowIsolationPoints.map((point: any, index) => (
  4556. <Select.Option key={point.pointId || point.id || `point-${index}`} value={point.pointId || point.id}>{point.pointName}</Select.Option>
  4557. ))}
  4558. </Select>
  4559. </div>
  4560. )}
  4561. {/* 盲板和拆除:显示负责人 */}
  4562. {/* 字典值:0=盲板,1=上锁挂牌,2=拆除 */}
  4563. {(workflowNodeConfig.isolationType === '0' || workflowNodeConfig.isolationType === '2') && (
  4564. <div>
  4565. <label className="block text-sm font-medium text-gray-700 mb-2">
  4566. 负责人 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
  4567. </label>
  4568. <Select
  4569. value={workflowNodeConfig.responsible || undefined}
  4570. onChange={(value) =>
  4571. setWorkflowNodeConfig({ ...workflowNodeConfig, responsible: value || undefined })
  4572. }
  4573. placeholder="请选择负责人"
  4574. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
  4575. allowClear
  4576. disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
  4577. >
  4578. {workflowDrawerUsers.map(user => (
  4579. <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
  4580. ))}
  4581. </Select>
  4582. </div>
  4583. )}
  4584. {/* 上锁挂牌:显示上锁人和共锁人 */}
  4585. {workflowNodeConfig.isolationType === '1' && (
  4586. <>
  4587. <div>
  4588. <label className="block text-sm font-medium text-gray-700 mb-2">
  4589. 上锁人 <span className="text-red-500" style={{ color: '#ef4444' }}>*</span>
  4590. </label>
  4591. <Select
  4592. value={workflowNodeConfig.lockPerson || undefined}
  4593. onChange={(value) =>
  4594. setWorkflowNodeConfig({ ...workflowNodeConfig, lockPerson: value || undefined })
  4595. }
  4596. placeholder="请选择上锁人"
  4597. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
  4598. allowClear
  4599. disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
  4600. >
  4601. {workflowLockerUsers.map(user => (
  4602. <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
  4603. ))}
  4604. </Select>
  4605. </div>
  4606. <div>
  4607. <label className="block text-sm font-medium text-gray-700 mb-2">
  4608. 共锁人(可多选)
  4609. </label>
  4610. <Select
  4611. mode="multiple"
  4612. value={workflowNodeConfig.coLockPersons}
  4613. onChange={(value) =>
  4614. setWorkflowNodeConfig({ ...workflowNodeConfig, coLockPersons: value })
  4615. }
  4616. placeholder="请选择共锁人"
  4617. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!min-h-10"
  4618. allowClear
  4619. disabled={isViewMode || selectedWorkflowNode.data?.type === 'releaseIsolation'}
  4620. >
  4621. {workflowColockerUsers.map(user => (
  4622. <Select.Option key={user.id} value={user.id}>{user.nickname || user.username}</Select.Option>
  4623. ))}
  4624. </Select>
  4625. </div>
  4626. </>
  4627. )}
  4628. </>
  4629. )}
  4630. </div>
  4631. </div>
  4632. )}
  4633. {/* 通知消息 - 创建作业节点不显示 */}
  4634. {selectedWorkflowNode.data?.type !== 'createJob' && (
  4635. <div>
  4636. <h4 className="flex items-center gap-3 text-base font-semibold text-gray-900 mb-4">
  4637. <span className="w-1 h-5 bg-blue-500 rounded-full flex-shrink-0" style={{ minWidth: '4px', minHeight: '20px' }}></span>
  4638. 通知消息
  4639. </h4>
  4640. <div className="space-y-5">
  4641. <div>
  4642. <label className="block text-sm font-medium text-gray-700 mb-3">
  4643. 通知方式
  4644. </label>
  4645. <div className="bg-gray-50 p-3 rounded-lg">
  4646. <div className="mb-3">
  4647. <Checkbox
  4648. checked={workflowNodeConfig.notificationMethods?.sms || false}
  4649. onChange={(e) =>
  4650. setWorkflowNodeConfig({
  4651. ...workflowNodeConfig,
  4652. notificationMethods: {
  4653. ...workflowNodeConfig.notificationMethods,
  4654. sms: e.target.checked,
  4655. },
  4656. smsTemplateCode: e.target.checked ? 'true' : 'false',
  4657. })
  4658. }
  4659. disabled={isViewMode}
  4660. >
  4661. 短信
  4662. </Checkbox>
  4663. </div>
  4664. <div className="mb-3">
  4665. <Checkbox
  4666. checked={workflowNodeConfig.notificationMethods?.message || false}
  4667. onChange={(e) =>
  4668. setWorkflowNodeConfig({
  4669. ...workflowNodeConfig,
  4670. notificationMethods: {
  4671. ...workflowNodeConfig.notificationMethods,
  4672. message: e.target.checked,
  4673. },
  4674. messageTemplateCode: e.target.checked ? 'true' : 'false',
  4675. })
  4676. }
  4677. disabled={isViewMode}
  4678. >
  4679. 站内信
  4680. </Checkbox>
  4681. </div>
  4682. <div className="mb-3">
  4683. <Checkbox
  4684. checked={workflowNodeConfig.notificationMethods?.email || false}
  4685. onChange={(e) =>
  4686. setWorkflowNodeConfig({
  4687. ...workflowNodeConfig,
  4688. notificationMethods: {
  4689. ...workflowNodeConfig.notificationMethods,
  4690. email: e.target.checked,
  4691. },
  4692. emailTemplateCode: e.target.checked ? 'true' : 'false',
  4693. })
  4694. }
  4695. disabled={isViewMode}
  4696. >
  4697. 邮件
  4698. </Checkbox>
  4699. </div>
  4700. <div>
  4701. <Checkbox
  4702. checked={workflowNodeConfig.notificationMethods?.app || false}
  4703. onChange={(e) =>
  4704. setWorkflowNodeConfig({
  4705. ...workflowNodeConfig,
  4706. notificationMethods: {
  4707. ...workflowNodeConfig.notificationMethods,
  4708. app: e.target.checked,
  4709. },
  4710. appTemplateCode: e.target.checked ? 'true' : 'false',
  4711. })
  4712. }
  4713. disabled={isViewMode}
  4714. >
  4715. APP通知
  4716. </Checkbox>
  4717. </div>
  4718. </div>
  4719. </div>
  4720. {/* <div>
  4721. <label className="block text-sm font-medium text-gray-700 mb-2">
  4722. 通知人
  4723. </label>
  4724. <Select
  4725. value={workflowNodeConfig.notificationPerson || undefined}
  4726. onChange={(value) =>
  4727. setWorkflowNodeConfig({ ...workflowNodeConfig, notificationPerson: value || '' })
  4728. }
  4729. placeholder="请选择通知人"
  4730. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
  4731. allowClear
  4732. disabled={isViewMode}
  4733. >
  4734. <Select.Option value="taskResponsible">任务负责人</Select.Option>
  4735. <Select.Option value="taskParticipant">任务参与人</Select.Option>
  4736. <Select.Option value="responsibleAndParticipant">负责人和参与人</Select.Option>
  4737. <Select.Option value="specifiedPerson">指定人</Select.Option>
  4738. </Select>
  4739. </div> */}
  4740. {/* <div>
  4741. <label className="block text-sm font-medium text-gray-700 mb-2">
  4742. 通知时间
  4743. </label>
  4744. <Select
  4745. value={workflowNodeConfig.notificationTime || undefined}
  4746. onChange={(value) =>
  4747. setWorkflowNodeConfig({ ...workflowNodeConfig, notificationTime: value || '' })
  4748. }
  4749. placeholder="请选择通知时间"
  4750. className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
  4751. allowClear
  4752. disabled={isViewMode}
  4753. >
  4754. <Select.Option value="before">执行前(上一个节点结束后)</Select.Option>
  4755. <Select.Option value="after">执行后(该节点结束后)</Select.Option>
  4756. <Select.Option value="time">选择时间</Select.Option>
  4757. <Select.Option value="30min">任务开始前30分钟</Select.Option>
  4758. <Select.Option value="1h">任务开始前1小时</Select.Option>
  4759. <Select.Option value="2h">任务开始前2小时</Select.Option>
  4760. <Select.Option value="4h">任务开始前4小时</Select.Option>
  4761. <Select.Option value="5h">任务开始前5小时</Select.Option>
  4762. <Select.Option value="8h">任务开始前8小时</Select.Option>
  4763. <Select.Option value="12h">任务开始前12小时</Select.Option>
  4764. <Select.Option value="24h">任务开始前24小时</Select.Option>
  4765. <Select.Option value="48h">任务开始前48小时</Select.Option>
  4766. </Select>
  4767. </div> */}
  4768. </div>
  4769. </div>
  4770. )}
  4771. </div>
  4772. {/* 保存按钮 */}
  4773. {!isViewMode && (
  4774. <div className="px-4 py-3 border-t border-gray-200 bg-white sticky bottom-0 z-10">
  4775. <Button
  4776. type="primary"
  4777. className="w-full"
  4778. onClick={async () => {
  4779. if (!selectedWorkflowNode) {
  4780. message.warning(t('common.pleaseSelectNodeToConfig'));
  4781. return;
  4782. }
  4783. // 验证当前节点配置
  4784. const isValid = validateNodeConfig(workflowNodeConfig, selectedWorkflowNode.data?.type || '');
  4785. if (!isValid) {
  4786. message.warning(t('common.pleaseCompleteRequiredNodeConfig'));
  4787. return;
  4788. }
  4789. try {
  4790. // 查找对应的 workflowWorkNodeDO 节点(通过uuid匹配)
  4791. const nodeDO = workflowWorkNodeDOList.find(item => item.uuid === selectedWorkflowNode.id);
  4792. // 在外部作用域定义,确保在自动切换节点时可以使用
  4793. let mergedWorkflowWorkNodeDOList: WorkflowWorkNodeDO[] = [];
  4794. if (nodeDO && nodeDO.id && workflowWorkId) {
  4795. // 构建节点用户信息列表
  4796. const nodeUserDOList: WorkflowWorkNodeUserDO[] = [];
  4797. // 如果是解除隔离节点,需要复制隔离节点的数据
  4798. if (selectedWorkflowNode.data?.type === 'releaseIsolation' && workflowNodeConfig.isolationNodeUuid) {
  4799. // 从 workflowWorkNodeDOList 中查找对应的隔离节点数据
  4800. const isolationNodeDO = workflowWorkNodeDOList.find(item =>
  4801. item.uuid === workflowNodeConfig.isolationNodeUuid && item.type === 'isolation'
  4802. );
  4803. if (isolationNodeDO) {
  4804. // 复制隔离节点的 nodeUserList(上锁人、共锁人等)
  4805. if (isolationNodeDO.nodeUserList && Array.isArray(isolationNodeDO.nodeUserList)) {
  4806. isolationNodeDO.nodeUserList.forEach((user: any) => {
  4807. if (user.type === 'jtlocker' || user.type === 'jtcolocker') {
  4808. nodeUserDOList.push({
  4809. userId: user.userId,
  4810. type: user.type,
  4811. });
  4812. }
  4813. });
  4814. }
  4815. }
  4816. } else {
  4817. // 如果不是解除隔离节点,使用原有逻辑
  4818. // 如果隔离方式是"上锁挂牌"(字典值:1),添加上锁人员和共锁人员
  4819. if (workflowNodeConfig.isolationType === '1') {
  4820. // 添加上锁人员
  4821. if (workflowNodeConfig.lockPerson) {
  4822. nodeUserDOList.push({
  4823. userId: Number(workflowNodeConfig.lockPerson),
  4824. type: 'jtlocker',
  4825. });
  4826. }
  4827. // 添加共锁人员(可以多选)
  4828. if (workflowNodeConfig.coLockPersons && workflowNodeConfig.coLockPersons.length > 0) {
  4829. workflowNodeConfig.coLockPersons.forEach((userId: string | number) => {
  4830. nodeUserDOList.push({
  4831. userId: Number(userId),
  4832. type: 'jtcolocker',
  4833. });
  4834. });
  4835. }
  4836. }
  4837. }
  4838. // 调用更新节点接口
  4839. const formId = workflowNodeConfig.submitForm ? Number(workflowNodeConfig.submitForm) : undefined;
  4840. // 如果 formId 存在,先调用接口获取表单数据
  4841. let formData: string | undefined = undefined;
  4842. if (formId) {
  4843. try {
  4844. const formResponse = await getForm(formId);
  4845. // 获取 data 字段的内容(如果接口返回的是 { code, data, message } 格式)
  4846. let formDataObj: any = formResponse;
  4847. if (formResponse && typeof formResponse === 'object' && 'data' in formResponse) {
  4848. formDataObj = (formResponse as any).data || formResponse;
  4849. }
  4850. // 将整个对象内容转换成 JSON 字符串传递给 formData 字段
  4851. formData = JSON.stringify(formDataObj);
  4852. console.log('表单数据已转换为字符串:', formData);
  4853. } catch (error) {
  4854. console.error('获取表单数据失败:', error);
  4855. // 即使获取失败,也继续保存节点,只是不传递 formData
  4856. }
  4857. }
  4858. const updateParam: UpdateWorkflowWorkNodeParam = {
  4859. nodeId: nodeDO.id,
  4860. nodeName: workflowNodeConfig.nodeName,
  4861. formId: formId,
  4862. workerUserId: workflowNodeConfig.responsible ? Number(workflowNodeConfig.responsible) : undefined,
  4863. isolationType: workflowNodeConfig.isolationType || undefined,
  4864. isolationPoints: workflowNodeConfig.isolationPoints && workflowNodeConfig.isolationPoints.length > 0
  4865. ? JSON.stringify(workflowNodeConfig.isolationPoints)
  4866. : undefined,
  4867. // 如果是解除隔离节点,传递选中的隔离节点UUID
  4868. isolationNodeUuid: (selectedWorkflowNode.data?.type === 'releaseIsolation' && workflowNodeConfig.isolationNodeUuid)
  4869. ? workflowNodeConfig.isolationNodeUuid
  4870. : undefined,
  4871. nodeUserDOList: nodeUserDOList.length > 0 ? nodeUserDOList : undefined,
  4872. // 如果获取到了表单数据,将整个对象内容转换成字符串传递给 formData 字段
  4873. formData: formData,
  4874. // 根据选中值传递模板代码参数(字符串类型的 'true' 或 'false')
  4875. smsTemplateCode: workflowNodeConfig.smsTemplateCode || 'false',
  4876. messageTemplateCode: workflowNodeConfig.messageTemplateCode || 'false',
  4877. emailTemplateCode: workflowNodeConfig.emailTemplateCode || 'false',
  4878. appTemplateCode: workflowNodeConfig.appTemplateCode || 'false',
  4879. };
  4880. await workJobApi.updateWorkflowWorkNode(updateParam);
  4881. // 更新成功后,立即调用详情接口获取最新数据
  4882. let mergedWorkflowWorkNodeDOList: WorkflowWorkNodeDO[] = [];
  4883. if (workflowWorkId) {
  4884. try {
  4885. const detailResponse = await workJobApi.selectWorkflowWorkById(workflowWorkId);
  4886. const detail = detailResponse as any;
  4887. if (detail.workflowWorkNodeDOList && Array.isArray(detail.workflowWorkNodeDOList)) {
  4888. // 使用接口返回的最新数据更新 workflowWorkNodeDOList
  4889. // 合并更新:保留现有数据,只更新接口返回的数据
  4890. // 先计算合并后的数据,然后再更新状态
  4891. const prev = workflowWorkNodeDOList;
  4892. console.log('合并更新前的 workflowWorkNodeDOList:', prev);
  4893. console.log('接口返回的 workflowWorkNodeDOList:', detail.workflowWorkNodeDOList);
  4894. // 创建一个映射,方便查找
  4895. const prevMap = new Map<string | number, WorkflowWorkNodeDO>();
  4896. prev.forEach(item => {
  4897. if (item.id) prevMap.set(item.id, item);
  4898. if (item.uuid) prevMap.set(item.uuid, item);
  4899. });
  4900. const updatedList = detail.workflowWorkNodeDOList.map((newItem: WorkflowWorkNodeDO) => {
  4901. // 查找现有列表中对应的节点(通过 id 或 uuid 匹配)
  4902. const existingItem = (newItem.id && prevMap.get(newItem.id)) ||
  4903. (newItem.uuid && prevMap.get(newItem.uuid)) ||
  4904. null;
  4905. // 如果找到现有节点,合并数据(优先使用接口返回的数据,但保留现有数据中接口没有返回的字段)
  4906. if (existingItem) {
  4907. // 合并数据,确保关键字段不被覆盖为空
  4908. // 只有当接口返回的值是有效的(不是 null、undefined、0 或空字符串)时,才使用接口的值
  4909. const merged: WorkflowWorkNodeDO = {
  4910. ...existingItem,
  4911. ...newItem,
  4912. // 如果接口返回的 workerUserId 是 null、undefined 或 0,保留现有的值
  4913. workerUserId: (newItem.workerUserId !== null && newItem.workerUserId !== undefined && newItem.workerUserId !== 0)
  4914. ? newItem.workerUserId
  4915. : existingItem.workerUserId,
  4916. // 如果接口返回的 formId 是 null、undefined 或 0,保留现有的值
  4917. formId: (newItem.formId !== null && newItem.formId !== undefined && newItem.formId !== 0)
  4918. ? newItem.formId
  4919. : existingItem.formId,
  4920. // 如果接口返回的 isolationType 是 null 或 undefined,保留现有的值
  4921. isolationType: (newItem.isolationType !== null && newItem.isolationType !== undefined && newItem.isolationType !== '')
  4922. ? newItem.isolationType
  4923. : existingItem.isolationType,
  4924. // 如果接口返回的 isolationPoints 是 null 或 undefined,保留现有的值
  4925. isolationPoints: (newItem.isolationPoints !== null && newItem.isolationPoints !== undefined && newItem.isolationPoints !== '')
  4926. ? newItem.isolationPoints
  4927. : existingItem.isolationPoints,
  4928. // 如果接口返回的 nodeUserList 是空数组或不存在,保留现有的值
  4929. nodeUserList: (newItem.nodeUserList && Array.isArray(newItem.nodeUserList) && newItem.nodeUserList.length > 0)
  4930. ? newItem.nodeUserList
  4931. : (existingItem.nodeUserList || []),
  4932. };
  4933. console.log(`合并节点 ${newItem.uuid || newItem.id}:`, {
  4934. existing: { workerUserId: existingItem.workerUserId, formId: existingItem.formId },
  4935. new: { workerUserId: newItem.workerUserId, formId: newItem.formId },
  4936. merged: { workerUserId: merged.workerUserId, formId: merged.formId }
  4937. });
  4938. return merged;
  4939. }
  4940. // 如果没找到,直接使用接口返回的数据
  4941. return newItem;
  4942. });
  4943. // 如果接口返回的列表中有新节点(不在现有列表中的),也要添加
  4944. const existingUuids = new Set<string | number>();
  4945. prev.forEach(item => {
  4946. if (item.id) existingUuids.add(item.id);
  4947. if (item.uuid) existingUuids.add(item.uuid);
  4948. });
  4949. const newItems = detail.workflowWorkNodeDOList.filter((item: WorkflowWorkNodeDO) => {
  4950. const itemKey = item.id || item.uuid;
  4951. return itemKey && !existingUuids.has(itemKey);
  4952. });
  4953. mergedWorkflowWorkNodeDOList = [...updatedList, ...newItems];
  4954. console.log('合并更新后的 workflowWorkNodeDOList:', mergedWorkflowWorkNodeDOList);
  4955. // 更新状态
  4956. setWorkflowWorkNodeDOList(mergedWorkflowWorkNodeDOList);
  4957. }
  4958. } catch (error: any) {
  4959. console.error('获取最新节点数据失败:', error);
  4960. // 如果接口调用失败,仍然使用本地更新逻辑作为后备
  4961. setWorkflowWorkNodeDOList(prev => prev.map(item => {
  4962. if (item.id === nodeDO.id) {
  4963. return {
  4964. ...item,
  4965. nodeName: workflowNodeConfig.nodeName,
  4966. formId: workflowNodeConfig.submitForm ? Number(workflowNodeConfig.submitForm) : undefined,
  4967. workerUserId: workflowNodeConfig.responsible ? Number(workflowNodeConfig.responsible) : undefined,
  4968. isolationType: workflowNodeConfig.isolationType || undefined,
  4969. isolationPoints: workflowNodeConfig.isolationPoints && workflowNodeConfig.isolationPoints.length > 0
  4970. ? JSON.stringify(workflowNodeConfig.isolationPoints)
  4971. : undefined,
  4972. lockPerson: workflowNodeConfig.lockPerson ? String(workflowNodeConfig.lockPerson) : undefined,
  4973. notifyTime: workflowNodeConfig.notificationTime || undefined,
  4974. data: JSON.stringify((() => {
  4975. const existingData = item.data ? (typeof item.data === 'string' ? JSON.parse(item.data) : item.data) : {};
  4976. const { isolationMethod, ...restExistingData } = existingData;
  4977. return {
  4978. ...restExistingData,
  4979. label: workflowNodeConfig.nodeName,
  4980. icon: workflowNodeConfig.nodeIcon,
  4981. responsible: workflowNodeConfig.responsible ? String(workflowNodeConfig.responsible) : '',
  4982. remark: workflowNodeConfig.remark,
  4983. submitForm: workflowNodeConfig.submitForm ? String(workflowNodeConfig.submitForm) : '',
  4984. isolationType: workflowNodeConfig.isolationType,
  4985. isolationPoints: workflowNodeConfig.isolationPoints,
  4986. isolationNode: workflowNodeConfig.isolationNode,
  4987. isolationNodeUuid: workflowNodeConfig.isolationNodeUuid,
  4988. lockPerson: workflowNodeConfig.lockPerson ? String(workflowNodeConfig.lockPerson) : '',
  4989. coLockPersons: workflowNodeConfig.coLockPersons.map((id: any) => String(id)),
  4990. notificationMethods: workflowNodeConfig.notificationMethods,
  4991. notificationPerson: workflowNodeConfig.notificationPerson,
  4992. notificationTime: workflowNodeConfig.notificationTime,
  4993. };
  4994. })()),
  4995. };
  4996. }
  4997. return item;
  4998. }));
  4999. }
  5000. }
  5001. }
  5002. // 缓存当前节点配置
  5003. const newCache = new Map(workflowNodeConfigCache);
  5004. newCache.set(selectedWorkflowNode.id, { ...workflowNodeConfig });
  5005. setWorkflowNodeConfigCache(newCache);
  5006. // 标记当前节点为已完成
  5007. const newCompleted = new Set(completedNodeIds);
  5008. newCompleted.add(selectedWorkflowNode.id);
  5009. setCompletedNodeIds(newCompleted);
  5010. // 更新节点保存状态缓存
  5011. const newSavedStatusCache = new Map(nodeSavedStatusCache);
  5012. newSavedStatusCache.set(selectedWorkflowNode.id, true);
  5013. setNodeSavedStatusCache(newSavedStatusCache);
  5014. // 更新节点显示
  5015. const { isolationMethod, ...restData } = selectedWorkflowNode.data || {};
  5016. const updatedData = {
  5017. ...restData,
  5018. label: workflowNodeConfig.nodeName,
  5019. icon: workflowNodeConfig.nodeIcon,
  5020. responsible: workflowNodeConfig.responsible,
  5021. remark: workflowNodeConfig.remark,
  5022. submitForm: workflowNodeConfig.submitForm,
  5023. isolationType: workflowNodeConfig.isolationType,
  5024. isolationPoints: workflowNodeConfig.isolationPoints,
  5025. isolationNode: workflowNodeConfig.isolationNode,
  5026. isolationNodeUuid: workflowNodeConfig.isolationNodeUuid,
  5027. lockPerson: workflowNodeConfig.lockPerson,
  5028. coLockPersons: workflowNodeConfig.coLockPersons,
  5029. notificationMethods: workflowNodeConfig.notificationMethods,
  5030. notificationPerson: workflowNodeConfig.notificationPerson,
  5031. notificationTime: workflowNodeConfig.notificationTime,
  5032. completed: true, // 标记为已完成
  5033. };
  5034. setWorkflowNodes((nds) =>
  5035. nds.map((node) =>
  5036. node.id === selectedWorkflowNode.id
  5037. ? { ...node, data: updatedData, selected: false }
  5038. : node
  5039. )
  5040. );
  5041. // 查找下一个未完成的节点(使用新的completedSet,而不是状态中的)
  5042. // 优先查找当前节点的子节点
  5043. const nextNode = getNextIncompleteNode(newCompleted, selectedWorkflowNode.id);
  5044. if (nextNode) {
  5045. // 自动切换到下一个节点
  5046. // 使用合并后的最新数据(如果有的话),否则使用状态中的数据
  5047. const latestWorkflowWorkNodeDOList = mergedWorkflowWorkNodeDOList.length > 0
  5048. ? mergedWorkflowWorkNodeDOList
  5049. : workflowWorkNodeDOList;
  5050. setTimeout(() => {
  5051. setSelectedWorkflowNode(nextNode);
  5052. const cachedConfig = newCache.get(nextNode.id);
  5053. if (cachedConfig) {
  5054. setWorkflowNodeConfig(cachedConfig);
  5055. } else {
  5056. // 从最新的 workflowWorkNodeDOList 中查找节点数据
  5057. const nextNodeDO = latestWorkflowWorkNodeDOList.find(item => item.uuid === nextNode.id);
  5058. if (nextNodeDO) {
  5059. let nextNodeData: any = {};
  5060. if (nextNodeDO.data) {
  5061. try {
  5062. nextNodeData = typeof nextNodeDO.data === 'string' ? JSON.parse(nextNodeDO.data) : nextNodeDO.data;
  5063. } catch (e) {
  5064. console.error('解析节点data失败:', e);
  5065. }
  5066. }
  5067. const source = nextNodeData || nextNode.data || {};
  5068. const config = nodeConfigs.find(c => c.type === (nextNodeDO.type || source.type));
  5069. setWorkflowNodeConfig({
  5070. nodeName: nextNodeDO.nodeName || source.label || config?.label || '',
  5071. nodeIcon: nextNodeDO.nodeIcon || source.icon || '',
  5072. responsible: (nextNodeDO.workerUserId !== null && nextNodeDO.workerUserId !== undefined && nextNodeDO.workerUserId !== 0)
  5073. ? (typeof nextNodeDO.workerUserId === 'number' ? nextNodeDO.workerUserId : Number(nextNodeDO.workerUserId))
  5074. : (source.workerUserId && source.workerUserId !== '' && source.workerUserId !== '0')
  5075. ? (typeof source.workerUserId === 'number' ? source.workerUserId : Number(source.workerUserId))
  5076. : undefined,
  5077. remark: source.remark || '',
  5078. submitForm: nextNodeDO.formId ? (typeof nextNodeDO.formId === 'number' ? nextNodeDO.formId : Number(nextNodeDO.formId)) : (source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined),
  5079. isolationType: (nextNodeDO.isolationType !== null && nextNodeDO.isolationType !== undefined) ? String(nextNodeDO.isolationType) : (source.isolationType || ''),
  5080. isolationPoints: nextNodeDO.isolationPoints ? (typeof nextNodeDO.isolationPoints === 'string' ? JSON.parse(nextNodeDO.isolationPoints) : nextNodeDO.isolationPoints) : (source.isolationPoints || []),
  5081. isolationNode: source.isolationNode || [],
  5082. isolationNodeUuid: nextNodeDO.isolationNodeUuid || source.isolationNodeUuid || '',
  5083. lockPerson: (() => {
  5084. // 优先从 nodeUserList 中提取
  5085. let lockPersonId: number | undefined = undefined;
  5086. if (nextNodeDO.nodeUserList && Array.isArray(nextNodeDO.nodeUserList)) {
  5087. const lockerUser = nextNodeDO.nodeUserList.find((user: any) => user.type === 'jtlocker');
  5088. if (lockerUser?.userId) {
  5089. lockPersonId = typeof lockerUser.userId === 'number' ? lockerUser.userId : Number(lockerUser.userId);
  5090. }
  5091. }
  5092. return lockPersonId || (source.lockPerson ? (typeof source.lockPerson === 'number' ? source.lockPerson : Number(source.lockPerson)) : undefined);
  5093. })(),
  5094. coLockPersons: (() => {
  5095. // 优先从 nodeUserList 中提取
  5096. const coLockPersonIds: number[] = [];
  5097. if (nextNodeDO.nodeUserList && Array.isArray(nextNodeDO.nodeUserList)) {
  5098. nextNodeDO.nodeUserList.forEach((user: any) => {
  5099. if (user.type === 'jtcolocker' && user.userId) {
  5100. coLockPersonIds.push(typeof user.userId === 'number' ? user.userId : Number(user.userId));
  5101. }
  5102. });
  5103. }
  5104. return coLockPersonIds.length > 0 ? coLockPersonIds : (source.coLockPersons && Array.isArray(source.coLockPersons) ? source.coLockPersons.map((id: any) => typeof id === 'number' ? id : Number(id)) : []);
  5105. })(),
  5106. notificationMethods: source.notificationMethods || {
  5107. sms: false,
  5108. message: false,
  5109. email: false,
  5110. app: false,
  5111. },
  5112. notificationPerson: source.notificationPerson || '',
  5113. notificationTime: nextNodeDO.notifyTime || source.notificationTime || '',
  5114. });
  5115. } else {
  5116. const source = nextNode.data || {};
  5117. const config = nodeConfigs.find(c => c.type === source.type);
  5118. setWorkflowNodeConfig({
  5119. nodeName: source.label || config?.label || '',
  5120. nodeIcon: source.icon || '',
  5121. responsible: source.responsible ? (typeof source.responsible === 'number' ? source.responsible : Number(source.responsible)) : undefined,
  5122. remark: source.remark || '',
  5123. submitForm: source.submitForm ? (typeof source.submitForm === 'number' ? source.submitForm : Number(source.submitForm)) : undefined,
  5124. isolationType: source.isolationType || '',
  5125. isolationPoints: source.isolationPoints || [],
  5126. isolationNode: source.isolationNode || [],
  5127. isolationNodeUuid: source.isolationNodeUuid || '',
  5128. lockPerson: source.lockPerson || '',
  5129. coLockPersons: source.coLockPersons || [],
  5130. notificationMethods: source.notificationMethods || {
  5131. sms: false,
  5132. message: false,
  5133. email: false,
  5134. app: false,
  5135. },
  5136. notificationPerson: source.notificationPerson || '',
  5137. notificationTime: source.notificationTime || '',
  5138. });
  5139. }
  5140. }
  5141. // 高亮下一个节点
  5142. setWorkflowNodes((nds) =>
  5143. nds.map((node) =>
  5144. node.id === nextNode.id
  5145. ? { ...node, selected: true }
  5146. : { ...node, selected: false }
  5147. )
  5148. );
  5149. // 不自动聚焦,保持当前视图,让用户能看到其他节点
  5150. // if (workflowReactFlowInstance) {
  5151. // workflowReactFlowInstance.fitView({ padding: 0.2, duration: 300, nodes: [nextNode] });
  5152. // }
  5153. }, 100);
  5154. message.success(t('common.nodeConfigSaved'));
  5155. } else {
  5156. // 所有节点都已完成,调用检查接口
  5157. if (workflowWorkId) {
  5158. try {
  5159. const checkResponse = await workJobApi.checkWorkById(workflowWorkId);
  5160. const checkData = (checkResponse as any)?.data || checkResponse;
  5161. const errorMessages = Array.isArray(checkData) ? checkData : (checkData ? [checkData] : []);
  5162. // 如果返回的错误信息数组为空或null,表示所有节点配置完毕
  5163. if (!errorMessages || errorMessages.length === 0) {
  5164. message.success(t('common.allNodeConfigCompleted'));
  5165. } else {
  5166. // 有错误信息,显示弹框
  5167. Modal.warning({
  5168. title: t('common.nodeConfigIncomplete'),
  5169. width: 500,
  5170. content: (
  5171. <div style={{ marginTop: 16 }}>
  5172. <p style={{ marginBottom: 12, color: '#666' }}>{t('common.nodeConfigIncompleteText')}</p>
  5173. <ul style={{ margin: 0, paddingLeft: 20 }}>
  5174. {errorMessages.map((msg: string, index: number) => (
  5175. <li key={index} style={{ marginBottom: 8, color: '#ff4d4f' }}>
  5176. {msg}
  5177. </li>
  5178. ))}
  5179. </ul>
  5180. </div>
  5181. ),
  5182. okText: t('common.iKnow'),
  5183. });
  5184. }
  5185. } catch (error: any) {
  5186. console.error('检查作业失败:', error);
  5187. message.error(error?.message || t('common.checkWorkFailed'));
  5188. }
  5189. } else {
  5190. message.success(t('common.allNodeConfigCompleted'));
  5191. }
  5192. }
  5193. } catch (error: any) {
  5194. console.error('保存节点配置失败:', error);
  5195. message.error(error?.message || t('common.saveFailed'));
  5196. }
  5197. }}
  5198. >
  5199. 保存当前节点
  5200. </Button>
  5201. </div>
  5202. )}
  5203. </div>
  5204. ) : (
  5205. <div className="p-4">
  5206. <div className="text-sm text-gray-600">
  5207. 请选择左侧流程中的节点进行配置
  5208. </div>
  5209. </div>
  5210. )}
  5211. </div>
  5212. </div>
  5213. )}
  5214. {workJobStep === 2 && (
  5215. <Form
  5216. form={workJobPublishForm}
  5217. layout="vertical"
  5218. className="max-w-2xl"
  5219. >
  5220. <div className="mb-4">
  5221. <h3 className="text-base font-medium text-gray-900 mb-2">发布作业设置</h3>
  5222. <p className="text-sm text-gray-500">请配置作业的执行时间并最终发布</p>
  5223. </div>
  5224. <Form.Item
  5225. label="发布方式"
  5226. name="startType"
  5227. required
  5228. rules={[{ required: true, message: '请选择发布方式' }]}
  5229. initialValue="0"
  5230. >
  5231. <Radio.Group disabled={isViewMode}>
  5232. <Radio value="0">立即发布</Radio>
  5233. <Radio value="1">定时发布</Radio>
  5234. </Radio.Group>
  5235. </Form.Item>
  5236. <Form.Item
  5237. noStyle
  5238. shouldUpdate={(prevValues, currentValues) =>
  5239. prevValues.startType !== currentValues.startType
  5240. }
  5241. >
  5242. {({ getFieldValue }) =>
  5243. getFieldValue('startType') === '1' ? (
  5244. <Form.Item
  5245. label="发布时间"
  5246. name="planTime"
  5247. required
  5248. rules={[{ required: true, message: '请选择发布时间' }]}
  5249. >
  5250. <DatePicker
  5251. showTime
  5252. format="YYYY-MM-DD HH:mm:ss"
  5253. className="w-full"
  5254. placeholder="请选择发布时间"
  5255. disabled={isViewMode}
  5256. />
  5257. </Form.Item>
  5258. ) : null
  5259. }
  5260. </Form.Item>
  5261. </Form>
  5262. )}
  5263. </div>
  5264. {/* 底部按钮 */}
  5265. <div className="px-6 py-4 border-t border-gray-200 flex justify-center gap-2">
  5266. {workJobStep === 0 && (
  5267. <>
  5268. <Button onClick={() => {
  5269. setShowAddModal(false);
  5270. setIsViewMode(false);
  5271. setWorkJobStep(0);
  5272. workJobBasicForm.resetFields();
  5273. workJobPublishForm.resetFields();
  5274. setWorkflowJson(null);
  5275. setWorkflowWorkId(null); // 清空作业ID
  5276. // 清空流程管理相关状态
  5277. setWorkflowNodes([]);
  5278. setWorkflowEdges([]);
  5279. setSelectedWorkflowNode(null);
  5280. setWorkflowNodeConfigCache(new Map());
  5281. setCompletedNodeIds(new Set());
  5282. setNodeSavedStatusCache(new Map());
  5283. setWorkflowWorkNodeDOList([]); // 清空节点数据列表
  5284. setOriginalBasicFormData(null); // 清空原始表单数据
  5285. // 关闭弹框时刷新列表
  5286. getWorkJobList();
  5287. }}>
  5288. {isViewMode ? '关闭' : '取消'}
  5289. </Button>
  5290. {!isViewMode ? (
  5291. <Button
  5292. type="primary"
  5293. onClick={async () => {
  5294. try {
  5295. // 验证必填项
  5296. const values = await workJobBasicForm.validateFields();
  5297. // 判断是新增还是更新
  5298. let currentWorkId: number | null = null;
  5299. if (workflowWorkId) {
  5300. // 已有ID,判断是否有修改
  5301. // 如果 originalBasicFormData 为 null,说明是新增后第一次返回,需要从服务器获取最新数据进行比较
  5302. let hasChanges = false;
  5303. if (originalBasicFormData) {
  5304. // 有原始数据,直接比较
  5305. hasChanges = (
  5306. originalBasicFormData.workflowTemplate !== values.workflowTemplate ||
  5307. originalBasicFormData.jobCategory !== values.jobCategory ||
  5308. originalBasicFormData.jobName !== values.jobName ||
  5309. originalBasicFormData.jobContent !== values.jobContent ||
  5310. originalBasicFormData.urgency !== values.urgency
  5311. );
  5312. } else {
  5313. // 没有原始数据,从服务器获取最新数据进行比较
  5314. try {
  5315. const detailResponse = await workJobApi.selectWorkflowWorkById(workflowWorkId);
  5316. const detail = detailResponse as any;
  5317. const currentFormData = {
  5318. workflowTemplate: values.workflowTemplate,
  5319. jobCategory: values.jobCategory,
  5320. jobName: values.jobName,
  5321. jobContent: values.jobContent,
  5322. urgency: values.urgency,
  5323. };
  5324. const serverData = {
  5325. workflowTemplate: detail.designId || detail.workflowTemplate,
  5326. jobCategory: detail.type || detail.jobCategory,
  5327. jobName: detail.name || detail.jobName,
  5328. jobContent: detail.description || detail.jobContent,
  5329. urgency: detail.urgencyLevel || detail.urgency,
  5330. };
  5331. hasChanges = (
  5332. serverData.workflowTemplate !== currentFormData.workflowTemplate ||
  5333. serverData.jobCategory !== currentFormData.jobCategory ||
  5334. serverData.jobName !== currentFormData.jobName ||
  5335. serverData.jobContent !== currentFormData.jobContent ||
  5336. serverData.urgency !== currentFormData.urgency
  5337. );
  5338. // 设置原始数据为服务器数据
  5339. setOriginalBasicFormData(serverData);
  5340. } catch (error: any) {
  5341. console.error('获取作业详情失败:', error);
  5342. // 如果获取失败,假设有变化,调用更新接口
  5343. hasChanges = true;
  5344. }
  5345. }
  5346. if (hasChanges) {
  5347. // 有修改,调用更新接口
  5348. await workJobApi.updateWorkflowWork({
  5349. id: workflowWorkId, // 作业票ID
  5350. name: values.jobName, // 作业名称
  5351. type: values.jobCategory, // 作业分类
  5352. designId: values.workflowTemplate, // 流程设计ID
  5353. description: values.jobContent, // 作业内容
  5354. urgencyLevel: values.urgency, // 紧急程度
  5355. });
  5356. message.success(t('common.basicInfoUpdateSuccess'));
  5357. // 更新原始数据
  5358. setOriginalBasicFormData({
  5359. workflowTemplate: values.workflowTemplate,
  5360. jobCategory: values.jobCategory,
  5361. jobName: values.jobName,
  5362. jobContent: values.jobContent,
  5363. urgency: values.urgency,
  5364. });
  5365. }
  5366. // 无论是否有修改,都使用当前的 workflowWorkId
  5367. currentWorkId = workflowWorkId;
  5368. } else {
  5369. // 没有ID,调用新增接口
  5370. const response = await workJobApi.insertWorkflowWork({
  5371. name: values.jobName, // 作业名称
  5372. type: values.jobCategory, // 作业分类
  5373. designId: values.workflowTemplate, // 流程设计ID
  5374. description: values.jobContent, // 作业内容
  5375. urgencyLevel: values.urgency, // 紧急程度
  5376. });
  5377. // 保存返回的作业ID
  5378. // 响应格式: { code: 0, data: 13, msg: "" }
  5379. // axios拦截器可能已经处理了数据格式,直接使用response
  5380. const responseData = response as any;
  5381. // 尝试多种方式获取ID
  5382. const workId = responseData?.data || responseData?.id || (typeof responseData === 'number' ? responseData : null);
  5383. if (workId) {
  5384. currentWorkId = Number(workId);
  5385. setWorkflowWorkId(currentWorkId);
  5386. console.log('保存作业ID:', workId);
  5387. }
  5388. // 新增成功后,设置原始表单数据,用于后续判断是否有修改
  5389. setOriginalBasicFormData({
  5390. workflowTemplate: values.workflowTemplate,
  5391. jobCategory: values.jobCategory,
  5392. jobName: values.jobName,
  5393. jobContent: values.jobContent,
  5394. urgency: values.urgency,
  5395. });
  5396. message.success(t('common.basicInfoSaveSuccess'));
  5397. }
  5398. // 成功后进入下一步
  5399. setWorkJobStep(1);
  5400. // 注意:节点状态初始化会在 useEffect 中自动执行(当 workJobStep === 1 且 workflowWorkId 存在时)
  5401. // 这里不需要手动加载,让 useEffect 统一处理
  5402. } catch (error: any) {
  5403. if (error.errorFields) {
  5404. // 表单验证失败
  5405. message.error(t('common.pleaseCompleteRequiredFields'));
  5406. } else {
  5407. // API调用失败
  5408. message.error(error?.message || t('common.saveFailed'));
  5409. }
  5410. }
  5411. }}
  5412. >
  5413. 下一步
  5414. </Button>
  5415. ) : (
  5416. <Button
  5417. type="primary"
  5418. onClick={() => setWorkJobStep(1)}
  5419. >
  5420. 下一步
  5421. </Button>
  5422. )}
  5423. </>
  5424. )}
  5425. {workJobStep === 1 && (
  5426. <div className="flex flex-col items-center w-full gap-2">
  5427. <div className="flex items-center gap-2">
  5428. <Button onClick={() => setWorkJobStep(0)}>
  5429. 上一步
  5430. </Button>
  5431. {!isViewMode && (
  5432. <>
  5433. <div className="flex items-center gap-2">
  5434. <Button
  5435. type="primary"
  5436. disabled={hasUnsavedNodes}
  5437. className={!hasUnsavedNodes ? 'next-step-button-focus' : ''}
  5438. onClick={async () => {
  5439. // 调用检查接口,检查是否有未保存的节点
  5440. if (!workflowWorkId) {
  5441. message.warning(t('common.workIdNotExistsForCheck'));
  5442. return;
  5443. }
  5444. try {
  5445. const checkResponse = await workJobApi.checkWorkById(workflowWorkId);
  5446. const checkData = (checkResponse as any)?.data || checkResponse;
  5447. const errorMessages = Array.isArray(checkData) ? checkData : (checkData ? [checkData] : []);
  5448. // 如果返回的错误信息数组为空或null,表示所有节点配置完毕,可以进入下一个tab
  5449. if (!errorMessages || errorMessages.length === 0) {
  5450. message.success(t('common.allNodeConfigCompleted'));
  5451. // 进入下一个tab
  5452. setWorkJobStep(2);
  5453. } else {
  5454. // 有错误信息,显示弹框,不能进入下一个tab
  5455. Modal.warning({
  5456. title: t('common.nodeConfigIncomplete'),
  5457. width: 500,
  5458. content: (
  5459. <div style={{ marginTop: 16 }}>
  5460. <p style={{ marginBottom: 12, color: '#666' }}>{t('common.nodeConfigIncompleteText')}</p>
  5461. <ul style={{ margin: 0, paddingLeft: 20 }}>
  5462. {errorMessages.map((msg: string, index: number) => (
  5463. <li key={index} style={{ marginBottom: 8, color: '#ff4d4f' }}>
  5464. {msg}
  5465. </li>
  5466. ))}
  5467. </ul>
  5468. </div>
  5469. ),
  5470. okText: t('common.iKnow'),
  5471. });
  5472. }
  5473. } catch (error: any) {
  5474. console.error('检查作业失败:', error);
  5475. message.error(error?.message || t('common.checkWorkFailed'));
  5476. }
  5477. }}
  5478. >
  5479. 下一步
  5480. </Button>
  5481. {!hasUnsavedNodes && (
  5482. <img
  5483. src={new URL('../assets/finger.png', import.meta.url).href}
  5484. alt="指向下一步"
  5485. className="pointer-hint-icon"
  5486. style={{
  5487. width: '32px',
  5488. height: '32px',
  5489. animation: 'pointing 1.5s ease-in-out infinite',
  5490. display: 'inline-block',
  5491. cursor: 'pointer',
  5492. marginLeft: '8px',
  5493. objectFit: 'contain'
  5494. }}
  5495. />
  5496. )}
  5497. <style>{`
  5498. @keyframes focusPulse {
  5499. 0%, 100% {
  5500. box-shadow: 0 0 0 0 rgba(22, 119, 255, 0.7);
  5501. transform: scale(1);
  5502. }
  5503. 50% {
  5504. box-shadow: 0 0 0 10px rgba(22, 119, 255, 0);
  5505. transform: scale(1.05);
  5506. }
  5507. }
  5508. .next-step-button-focus {
  5509. animation: focusPulse 2s ease-in-out infinite;
  5510. }
  5511. @keyframes pointing {
  5512. 0%, 100% {
  5513. transform: translateX(0);
  5514. }
  5515. 25% {
  5516. transform: translateX(-4px);
  5517. }
  5518. 50% {
  5519. transform: translateX(0);
  5520. }
  5521. 75% {
  5522. transform: translateX(4px);
  5523. }
  5524. }
  5525. .pointer-hint-icon {
  5526. cursor: pointer;
  5527. }
  5528. `}</style>
  5529. </div>
  5530. {/* <Button
  5531. type="default"
  5532. onClick={async () => {
  5533. if (!workflowWorkId) {
  5534. message.warning(t('common.workIdNotExistsForCheck'));
  5535. return;
  5536. }
  5537. try {
  5538. const checkResponse = await workJobApi.checkWorkById(workflowWorkId);
  5539. const checkData = (checkResponse as any)?.data || checkResponse;
  5540. const errorMessages = Array.isArray(checkData) ? checkData : (checkData ? [checkData] : []);
  5541. // 如果返回的错误信息数组为空或null,表示所有节点配置完毕,可以跳转
  5542. if (!errorMessages || errorMessages.length === 0) {
  5543. message.success(t('common.allNodeConfigCompleted'));
  5544. // 延迟一下再跳转,让用户看到成功提示
  5545. setTimeout(() => {
  5546. setWorkJobStep(2);
  5547. }, 500);
  5548. } else {
  5549. // 有错误信息,显示弹框,不跳转
  5550. Modal.warning({
  5551. title: t('common.nodeConfigIncomplete'),
  5552. width: 500,
  5553. content: (
  5554. <div style={{ marginTop: 16 }}>
  5555. <p style={{ marginBottom: 12, color: '#666' }}>{t('common.nodeConfigIncompleteText')}</p>
  5556. <ul style={{ margin: 0, paddingLeft: 20 }}>
  5557. {errorMessages.map((msg: string, index: number) => (
  5558. <li key={index} style={{ marginBottom: 8, color: '#ff4d4f' }}>
  5559. {msg}
  5560. </li>
  5561. ))}
  5562. </ul>
  5563. </div>
  5564. ),
  5565. okText: t('common.iKnow'),
  5566. });
  5567. }
  5568. } catch (error: any) {
  5569. console.error('检查作业失败:', error);
  5570. message.error(error?.message || t('common.checkWorkFailed'));
  5571. }
  5572. }}
  5573. >
  5574. 一键保存
  5575. </Button> */}
  5576. </>
  5577. )}
  5578. {isViewMode && (
  5579. <Button
  5580. type="primary"
  5581. onClick={() => setWorkJobStep(2)}
  5582. >
  5583. 下一步
  5584. </Button>
  5585. )}
  5586. </div>
  5587. <div className="flex items-center text-red-600" style={{ fontSize: '12px' }}>
  5588. <span className="mr-1">⚠️</span>
  5589. <span>提示:流程中的每一个节点都需要单独配置并保存,方可点击 "下一步"。</span>
  5590. </div>
  5591. </div>
  5592. )}
  5593. {workJobStep === 2 && (
  5594. <>
  5595. <Button onClick={() => setWorkJobStep(1)}>
  5596. 上一步
  5597. </Button>
  5598. {!isViewMode ? (
  5599. <Button
  5600. type="primary"
  5601. icon={<Play className="w-4 h-4" />}
  5602. onClick={async () => {
  5603. try {
  5604. const publishValues = await workJobPublishForm.validateFields();
  5605. if (!workflowWorkId) {
  5606. message.error(t('common.workIdNotExists'));
  5607. return;
  5608. }
  5609. // 构建发布参数
  5610. const updateStartWorkParam: UpdateStartWorkParam = {
  5611. workId: Number(workflowWorkId), // 确保是 number 类型
  5612. startType: publishValues.startType || '0', // 0-立即发布 1-定时发布
  5613. };
  5614. // 如果是定时发布,需要传递发布时间
  5615. if (publishValues.startType === '1') {
  5616. if (!publishValues.planTime) {
  5617. message.error(t('common.scheduledPublishTimeRequired'));
  5618. return;
  5619. }
  5620. // 将 DatePicker 返回的 dayjs 对象转换为字符串格式 (YYYY-MM-DD HH:mm:ss)
  5621. const planTime = publishValues.planTime;
  5622. if (planTime && typeof planTime.format === 'function') {
  5623. // dayjs 对象
  5624. updateStartWorkParam.planTime = planTime.format('YYYY-MM-DD HH:mm:ss');
  5625. } else if (planTime instanceof Date) {
  5626. // Date 对象
  5627. const year = planTime.getFullYear();
  5628. const month = String(planTime.getMonth() + 1).padStart(2, '0');
  5629. const day = String(planTime.getDate()).padStart(2, '0');
  5630. const hours = String(planTime.getHours()).padStart(2, '0');
  5631. const minutes = String(planTime.getMinutes()).padStart(2, '0');
  5632. const seconds = String(planTime.getSeconds()).padStart(2, '0');
  5633. updateStartWorkParam.planTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  5634. } else if (typeof planTime === 'string') {
  5635. // 字符串格式
  5636. updateStartWorkParam.planTime = planTime;
  5637. } else {
  5638. message.error(t('common.publishTimeFormatError'));
  5639. return;
  5640. }
  5641. }
  5642. console.log('发布作业参数:', updateStartWorkParam);
  5643. // 调用发布作业接口
  5644. await workJobApi.updateStartWork(updateStartWorkParam);
  5645. message.success(t('common.publishWorkSuccess'));
  5646. setShowAddModal(false);
  5647. setWorkJobStep(0);
  5648. workJobBasicForm.resetFields();
  5649. workJobPublishForm.resetFields();
  5650. setWorkflowJson(null);
  5651. setWorkflowWorkId(null); // 清空作业ID
  5652. // 清空流程管理相关状态
  5653. setWorkflowNodes([]);
  5654. setWorkflowEdges([]);
  5655. setSelectedWorkflowNode(null);
  5656. setWorkflowNodeConfigCache(new Map());
  5657. setCompletedNodeIds(new Set());
  5658. // 清除节点保存状态缓存
  5659. setNodeSavedStatusCache(new Map());
  5660. // 关闭弹框时刷新列表
  5661. getWorkJobList();
  5662. } catch (error: any) {
  5663. if (error.errorFields) {
  5664. return;
  5665. }
  5666. console.error('发布作业失败:', error);
  5667. message.error(error?.message || t('common.publishWorkFailed'));
  5668. }
  5669. }}
  5670. >
  5671. 确认发布
  5672. </Button>
  5673. ) : (
  5674. <Button onClick={() => {
  5675. setShowAddModal(false);
  5676. setIsViewMode(false);
  5677. setWorkJobStep(0);
  5678. workJobBasicForm.resetFields();
  5679. workJobPublishForm.resetFields();
  5680. setWorkflowJson(null);
  5681. setWorkflowWorkId(null);
  5682. setWorkflowNodes([]);
  5683. setWorkflowEdges([]);
  5684. setSelectedWorkflowNode(null);
  5685. setWorkflowNodeConfigCache(new Map());
  5686. setCompletedNodeIds(new Set());
  5687. setNodeSavedStatusCache(new Map());
  5688. setWorkflowWorkNodeDOList([]);
  5689. setOriginalBasicFormData(null);
  5690. getWorkJobList();
  5691. }}>
  5692. 关闭
  5693. </Button>
  5694. )}
  5695. </>
  5696. )}
  5697. </div>
  5698. </div>
  5699. </Modal>
  5700. ) : null}
  5701. {/* 编辑作业弹窗(保持原有逻辑) */}
  5702. {subMenu === '作业管理' && showAddModal && editingItem ? (
  5703. <Modal
  5704. title={t('common.editWork')}
  5705. open={showAddModal}
  5706. onCancel={() => {
  5707. setShowAddModal(false);
  5708. setEditingItem(null);
  5709. form.resetFields();
  5710. }}
  5711. footer={[
  5712. <Button key="cancel" onClick={() => {
  5713. setShowAddModal(false);
  5714. setEditingItem(null);
  5715. form.resetFields();
  5716. }}>
  5717. {t('common.cancel')}
  5718. </Button>,
  5719. <Button key="submit" type="primary" onClick={async () => {
  5720. try {
  5721. const values = await form.validateFields();
  5722. await workJobApi.updateWorkflowWork({
  5723. id: editingItem.id!,
  5724. name: values.name,
  5725. type: values.type || editingItem.type || '',
  5726. designId: values.designId || editingItem.designId || editingItem.workflowDesignId || 0,
  5727. description: values.content || '',
  5728. urgencyLevel: values.urgencyLevel || editingItem.urgencyLevel || '',
  5729. });
  5730. message.success(t('common.updateWorkSuccess'));
  5731. setShowAddModal(false);
  5732. setEditingItem(null);
  5733. form.resetFields();
  5734. getWorkJobList();
  5735. } catch (error: any) {
  5736. if (error.errorFields) {
  5737. return;
  5738. }
  5739. console.error('更新作业失败:', error);
  5740. message.error(error?.message || t('common.updateWorkFailed'));
  5741. }
  5742. }}>
  5743. {t('common.confirm')}
  5744. </Button>
  5745. ]}
  5746. width={600}
  5747. destroyOnClose
  5748. styles={{
  5749. footer: {
  5750. display: 'flex',
  5751. justifyContent: 'center',
  5752. alignItems: 'center',
  5753. padding: '16px 24px'
  5754. }
  5755. }}
  5756. >
  5757. <Form
  5758. form={form}
  5759. labelCol={{ span: 4 }}
  5760. wrapperCol={{ span: 20 }}
  5761. key={editingItem?.id || 'new'}
  5762. >
  5763. <Form.Item
  5764. label={t('common.workName')}
  5765. name="name"
  5766. rules={[{ required: true, message: t('common.workNameRequired') }]}
  5767. >
  5768. <Input placeholder={t('common.workNamePlaceholder')} />
  5769. </Form.Item>
  5770. <Form.Item
  5771. label={t('common.workContent')}
  5772. name="content"
  5773. >
  5774. <Input.TextArea rows={4} placeholder={t('common.workContentPlaceholder')} />
  5775. </Form.Item>
  5776. </Form>
  5777. </Modal>
  5778. ) : null}
  5779. </div>
  5780. );
  5781. }
  5782. return (
  5783. <div className="space-y-6">
  5784. {/* 表格容器 - 合并工具栏和列表 */}
  5785. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
  5786. {/* 工具栏 */}
  5787. <div className="p-4 border-b border-gray-200/50">
  5788. <div className="flex items-center justify-between">
  5789. <div className="relative w-80">
  5790. <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
  5791. <input
  5792. type="text"
  5793. placeholder={subMenu === '流程模板' ? t('form.searchTemplate') : `搜索${subMenu}...`}
  5794. value={searchTerm}
  5795. onChange={(e) => setSearchTerm(e.target.value)}
  5796. 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"
  5797. />
  5798. </div>
  5799. <button
  5800. onClick={() => {
  5801. setEditingItem(null);
  5802. setShowAddModal(true);
  5803. }}
  5804. 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"
  5805. >
  5806. <Plus className="w-4 h-4" strokeWidth={2.5} />
  5807. <span className="text-sm">
  5808. {subMenu === '流程模板' && t('common.addTemplate')}
  5809. {subMenu === 'SOP管理' && '新增SOP'}
  5810. {subMenu === '作业管理' && t('common.addWork')}
  5811. </span>
  5812. </button>
  5813. </div>
  5814. </div>
  5815. {/* 表格 */}
  5816. <div className="overflow-x-auto">
  5817. <table className="w-full">
  5818. <thead>
  5819. <tr className="bg-gradient-to-r from-gray-50 to-gray-100/50 border-b border-gray-200">
  5820. <th className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider" style={{ width: '5%' }}>
  5821. {t('common.serialNumber')}
  5822. </th>
  5823. {columns.map((column) => (
  5824. <th
  5825. key={column.key}
  5826. className="px-6 py-4 text-left text-xs text-gray-600 uppercase tracking-wider"
  5827. style={{ width: column.width }}
  5828. >
  5829. {column.label}
  5830. </th>
  5831. ))}
  5832. <th className="px-6 py-4 text-center text-xs text-gray-600 uppercase tracking-wider" style={{ width: '10%' }}>
  5833. {t('table.operation')}
  5834. </th>
  5835. </tr>
  5836. </thead>
  5837. <tbody className="divide-y divide-gray-100">
  5838. {filteredData.map((row, index) => (
  5839. <tr
  5840. key={row.id}
  5841. className="hover:bg-blue-50/30 transition-colors"
  5842. >
  5843. <td className="px-6 py-4 text-sm text-gray-900">
  5844. {index + 1}
  5845. </td>
  5846. {columns.map((column) => (
  5847. <td key={column.key} className="px-6 py-4 text-sm text-gray-900">
  5848. {column.key === 'status' ? (
  5849. <span
  5850. className={`inline-flex px-3 py-1 rounded-lg text-xs ${
  5851. row[column.key] === '启用' || row[column.key] === '有效' || row[column.key] === '已完成'
  5852. ? 'bg-green-100 text-green-700'
  5853. : row[column.key] === '待审批' || row[column.key] === '待审核'
  5854. ? 'bg-orange-100 text-orange-700'
  5855. : row[column.key] === '进行中'
  5856. ? 'bg-blue-100 text-blue-700'
  5857. : 'bg-gray-100 text-gray-700'
  5858. }`}
  5859. >
  5860. {row[column.key]}
  5861. </span>
  5862. ) : column.key === 'priority' ? (
  5863. <span
  5864. className={`inline-flex px-3 py-1 rounded-lg text-xs ${
  5865. row[column.key] === '紧急'
  5866. ? 'bg-red-100 text-red-700'
  5867. : row[column.key] === '重要'
  5868. ? 'bg-orange-100 text-orange-700'
  5869. : 'bg-blue-100 text-blue-700'
  5870. }`}
  5871. >
  5872. {row[column.key]}
  5873. </span>
  5874. ) : (
  5875. row[column.key]
  5876. )}
  5877. </td>
  5878. ))}
  5879. <td className="px-6 py-4">
  5880. <div className="flex items-center justify-center gap-2">
  5881. <Button
  5882. variant="ghost"
  5883. size="sm"
  5884. className="h-8 px-2"
  5885. >
  5886. <Eye className="w-4 h-4" />
  5887. <span className="ml-1">{t('table.view')}</span>
  5888. </Button>
  5889. <Button
  5890. variant="ghost"
  5891. size="sm"
  5892. onClick={() => handleEdit(row)}
  5893. className="h-8 px-2"
  5894. >
  5895. <Edit2 className="w-4 h-4" />
  5896. <span className="ml-1">{t('common.edit')}</span>
  5897. </Button>
  5898. <Button
  5899. variant="ghost"
  5900. size="sm"
  5901. onClick={() => handleDelete(row.id)}
  5902. className="h-8 px-2 text-red-600 hover:text-red-700"
  5903. >
  5904. <Trash2 className="w-4 h-4" />
  5905. <span className="ml-1">{t('common.delete')}</span>
  5906. </Button>
  5907. </div>
  5908. </td>
  5909. </tr>
  5910. ))}
  5911. </tbody>
  5912. </table>
  5913. </div>
  5914. </div>
  5915. {/* 分页 */}
  5916. {filteredData.length > 0 && (
  5917. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  5918. <div className="flex items-center justify-between">
  5919. <div className="text-sm text-gray-600">
  5920. {t('common.total')} <span className="text-blue-600 font-medium">{filteredData.length}</span> {t('common.records')}
  5921. </div>
  5922. <div className="flex gap-2">
  5923. <Button
  5924. disabled={true}
  5925. >
  5926. {t('common.prevPage')}
  5927. </Button>
  5928. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  5929. 1 / 1
  5930. </span>
  5931. <Button
  5932. disabled={true}
  5933. >
  5934. {t('common.nextPage')}
  5935. </Button>
  5936. </div>
  5937. </div>
  5938. </div>
  5939. )}
  5940. {/* 新增/编辑弹窗 */}
  5941. {showAddModal && (
  5942. <div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
  5943. <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">
  5944. {/* 弹窗标题 */}
  5945. <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between sticky top-0 bg-white z-10">
  5946. <h3 className="text-lg text-gray-900">
  5947. {editingItem
  5948. ? (subMenu === '流程模板' ? t('common.editTemplate') : subMenu === 'SOP管理' ? '编辑SOP' : subMenu === '作业管理' ? t('common.editWork') : subMenu === '表单管理' ? t('common.editFormManagement') : subMenu === '流程设计' ? t('common.editProcessDesign') : t('common.edit'))
  5949. : (subMenu === '流程模板' ? t('common.addTemplateTitle') : subMenu === 'SOP管理' ? '新增SOP' : subMenu === '作业管理' ? t('common.addWork') : subMenu === '表单管理' ? t('common.addFormManagement') : subMenu === '流程设计' ? t('common.addProcessDesign') : t('common.addNew'))
  5950. }
  5951. </h3>
  5952. <button
  5953. onClick={() => setShowAddModal(false)}
  5954. className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
  5955. >
  5956. <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  5957. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
  5958. </svg>
  5959. </button>
  5960. </div>
  5961. {/* 弹窗内容 */}
  5962. <div className="px-6 py-6">
  5963. {getFormFields()}
  5964. </div>
  5965. {/* 弹窗底部 */}
  5966. <div className="px-6 py-4 bg-gray-50/50 border-t border-gray-200 flex justify-end gap-3 rounded-b-2xl">
  5967. <button
  5968. onClick={() => setShowAddModal(false)}
  5969. 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"
  5970. >
  5971. {t('common.cancel')}
  5972. </button>
  5973. <button
  5974. onClick={() => {
  5975. console.log('保存:', editingItem);
  5976. setShowAddModal(false);
  5977. }}
  5978. 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"
  5979. >
  5980. {editingItem ? t('common.saveChanges') : t('common.confirm')}
  5981. </button>
  5982. </div>
  5983. </div>
  5984. </div>
  5985. )}
  5986. {/* 查看详情弹框(只读模式,三个tab) */}
  5987. <Modal
  5988. title={null}
  5989. open={showViewModal}
  5990. onCancel={() => {
  5991. setShowViewModal(false);
  5992. setViewWorkDetailData(null);
  5993. setViewWorkJobStep(0);
  5994. viewWorkJobBasicForm.resetFields();
  5995. viewWorkJobPublishForm.resetFields();
  5996. setViewWorkflowJson(null);
  5997. }}
  5998. footer={null}
  5999. width={1400}
  6000. destroyOnClose
  6001. styles={{
  6002. body: { padding: 0 }
  6003. }}
  6004. >
  6005. <div className="flex flex-col h-[80vh]">
  6006. {/* Tab导航 */}
  6007. <div className="px-6 py-4 bg-white">
  6008. <Tabs
  6009. activeKey={String(viewWorkJobStep)}
  6010. onChange={(key) => {
  6011. // 查看模式下可以自由切换tab
  6012. setViewWorkJobStep(Number(key));
  6013. }}
  6014. items={[
  6015. {
  6016. key: '0',
  6017. label: (
  6018. <span className="flex items-center gap-2">
  6019. <FileText className="w-4 h-4" />
  6020. 基本信息
  6021. </span>
  6022. ),
  6023. },
  6024. {
  6025. key: '1',
  6026. label: (
  6027. <span className="flex items-center gap-2">
  6028. <Workflow className="w-4 h-4" />
  6029. 流程管理
  6030. </span>
  6031. ),
  6032. },
  6033. {
  6034. key: '2',
  6035. label: (
  6036. <span className="flex items-center gap-2">
  6037. <Play className="w-4 h-4" />
  6038. 发布作业
  6039. </span>
  6040. ),
  6041. },
  6042. ]}
  6043. />
  6044. </div>
  6045. {/* Tab内容 */}
  6046. <div className="flex-1 overflow-y-auto px-6 py-4 bg-white">
  6047. {viewWorkJobStep === 0 && (
  6048. <div className="flex justify-center">
  6049. <Form
  6050. form={viewWorkJobBasicForm}
  6051. layout="vertical"
  6052. className="max-w-2xl w-full"
  6053. >
  6054. <Form.Item
  6055. label="流程模板"
  6056. name="workflowTemplate"
  6057. >
  6058. <Select
  6059. disabled
  6060. placeholder="请选择流程模板"
  6061. options={workflowTemplateList
  6062. .filter(item => item.status === 1)
  6063. .map(item => ({
  6064. label: item.name,
  6065. value: item.id,
  6066. }))}
  6067. />
  6068. </Form.Item>
  6069. <Form.Item
  6070. label="作业分类"
  6071. name="jobCategory"
  6072. >
  6073. <Select disabled placeholder="请选择作业分类">
  6074. {workTypeDictList.map((item) => (
  6075. <Select.Option key={item.id} value={item.value}>
  6076. {item.label}
  6077. </Select.Option>
  6078. ))}
  6079. </Select>
  6080. </Form.Item>
  6081. <Form.Item
  6082. label="作业名称"
  6083. name="jobName"
  6084. >
  6085. <Input disabled placeholder="请输入作业名称" />
  6086. </Form.Item>
  6087. <Form.Item
  6088. label="作业内容"
  6089. name="jobContent"
  6090. >
  6091. <Input.TextArea
  6092. disabled
  6093. rows={4}
  6094. placeholder="请输入作业内容"
  6095. showCount
  6096. maxLength={500}
  6097. />
  6098. </Form.Item>
  6099. <Form.Item
  6100. label="紧急程度"
  6101. name="urgency"
  6102. >
  6103. <Radio.Group disabled>
  6104. {urgencyLevelDictList.length > 0 ? (
  6105. urgencyLevelDictList.map((item) => (
  6106. <Radio key={item.id} value={item.value}>
  6107. {item.label}
  6108. </Radio>
  6109. ))
  6110. ) : (
  6111. <span className="text-gray-400 text-sm">加载中...</span>
  6112. )}
  6113. </Radio.Group>
  6114. </Form.Item>
  6115. </Form>
  6116. </div>
  6117. )}
  6118. {viewWorkJobStep === 1 && (
  6119. <div className="flex items-center justify-center h-full text-gray-400">
  6120. {viewWorkflowJson ? '流程管理视图(只读模式)' : '暂无流程设计数据'}
  6121. </div>
  6122. )}
  6123. {viewWorkJobStep === 2 && (
  6124. <Form
  6125. form={viewWorkJobPublishForm}
  6126. layout="vertical"
  6127. className="max-w-2xl"
  6128. >
  6129. <div className="mb-4">
  6130. <h3 className="text-base font-medium text-gray-900 mb-2">发布作业设置</h3>
  6131. <p className="text-sm text-gray-500">请配置作业的执行时间并最终发布</p>
  6132. </div>
  6133. <Form.Item
  6134. label="发布方式"
  6135. name="startType"
  6136. >
  6137. <Radio.Group disabled>
  6138. <Radio value="0">立即发布</Radio>
  6139. <Radio value="1">定时发布</Radio>
  6140. </Radio.Group>
  6141. </Form.Item>
  6142. <Form.Item
  6143. noStyle
  6144. shouldUpdate={(prevValues, currentValues) =>
  6145. prevValues.startType !== currentValues.startType
  6146. }
  6147. >
  6148. {({ getFieldValue }) =>
  6149. getFieldValue('startType') === '1' ? (
  6150. <Form.Item
  6151. label="发布时间"
  6152. name="planTime"
  6153. >
  6154. <DatePicker
  6155. disabled
  6156. showTime
  6157. format="YYYY-MM-DD HH:mm:ss"
  6158. className="w-full"
  6159. placeholder="请选择发布时间"
  6160. />
  6161. </Form.Item>
  6162. ) : null
  6163. }
  6164. </Form.Item>
  6165. </Form>
  6166. )}
  6167. </div>
  6168. {/* 底部按钮 */}
  6169. <div className="px-6 py-4 border-t border-gray-200 flex justify-end gap-2">
  6170. <Button onClick={() => {
  6171. setShowViewModal(false);
  6172. setViewWorkDetailData(null);
  6173. setViewWorkJobStep(0);
  6174. viewWorkJobBasicForm.resetFields();
  6175. viewWorkJobPublishForm.resetFields();
  6176. setViewWorkflowJson(null);
  6177. }}>
  6178. 关闭
  6179. </Button>
  6180. </div>
  6181. </div>
  6182. </Modal>
  6183. </div>
  6184. );
  6185. }