cleaning.vue.phase0 325 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214
  1. <template>
  2. <div :class="prefixCls" class="case-data-cleaning-page">
  3. <div class="page-header">
  4. <div class="page-header-main">
  5. <div class="page-title-area">
  6. <Button type="text" class="back-btn" @click="handleBack">←</Button>
  7. <div class="page-title-block">
  8. <div class="page-title">案件数据清洗</div>
  9. </div>
  10. </div>
  11. <div class="page-header-actions">
  12. <Button @click="toggleLeft">{{ leftCollapsed ? '展开左侧' : '收起左侧' }}</Button>
  13. <Button type="primary" :loading="importingData" @click="handleImportData">导入数据</Button>
  14. </div>
  15. </div>
  16. </div>
  17. <div class="main-layout" :style="{ gridTemplateColumns: layoutTemplateColumns }">
  18. <aside v-show="!leftCollapsed" class="left-panel">
  19. <div class="tree-header">
  20. <span class="label">文件分类</span>
  21. <div class="actions">
  22. <Button size="small" @click="sortMode = sortMode === 'asc' ? '' : 'asc'">升序</Button>
  23. <Button size="small" @click="sortMode = sortMode === 'desc' ? '' : 'desc'">降序</Button>
  24. </div>
  25. </div>
  26. <div class="tree-wrap">
  27. <BasicTree
  28. class="cleaning-file-tree"
  29. :tree-data="treeData"
  30. :tree-data-simple-mode="false"
  31. :field-names="{ key: 'key', title: 'title', children: 'children' }"
  32. :default-expand-all="true"
  33. v-model:selectedKeys="selectedKeys"
  34. @select="handleSelect"
  35. >
  36. <template #title="node">
  37. <div v-if="isTreeFileNode(node.raw)" class="tree-file-row" :title="getTreeFileName(node.raw)">
  38. <span class="tree-file-name">{{ getTreeFileName(node.raw) }}</span>
  39. </div>
  40. <div
  41. v-else
  42. class="tree-sheet-row"
  43. :class="{
  44. 'is-success': getSheetNodeVisualState(node.raw) === 'success',
  45. 'is-fail': getSheetNodeVisualState(node.raw) === 'fail',
  46. 'is-analyzing': getSheetNodeVisualState(node.raw) === 'analyzing',
  47. }"
  48. :title="getTreeSheetTitle(node.raw)"
  49. >
  50. <span class="tree-sheet-status">
  51. <span
  52. v-if="getSheetNodeVisualState(node.raw) === 'analyzing'"
  53. class="tree-sheet-status-spinner"
  54. ></span>
  55. <template v-else>
  56. {{ getSheetNodeVisualState(node.raw) === 'success' ? '✓' : '✕' }}
  57. </template>
  58. </span>
  59. <span class="tree-sheet-name">{{ getTreeSheetName(node.raw) }}</span>
  60. </div>
  61. </template>
  62. </BasicTree>
  63. </div>
  64. <div class="legend">
  65. <div v-for="item in iconTips" :key="item.key" class="legend-item">
  66. <span class="legend-icon" :class="`is-${item.key}`">
  67. <span v-if="item.key === 'matched' || item.key === 'unmatched'" class="legend-dot"></span>
  68. <template v-else>{{ item.icon }}</template>
  69. </span>
  70. <span class="legend-text">{{ item.label }}</span>
  71. </div>
  72. </div>
  73. </aside>
  74. <div v-show="!leftCollapsed" class="panel-resizer" @mousedown="startResize('left', $event)"></div>
  75. <section class="middle-panel">
  76. <div class="template-match-bar" :class="{ 'is-empty': !hasTemplateSelected }">
  77. <div class="template-match-main">
  78. <span class="template-match-icon"></span>
  79. <div class="template-match-copy">
  80. <div class="template-match-label">匹配模板</div>
  81. <div class="template-match-name">
  82. {{ currentTemplateDisplayName }}
  83. <span v-if="!hasTemplateSelected" class="template-match-badge is-pending">待配置</span>
  84. <span v-else-if="templateDetailFromMock" class="template-match-badge">示例</span>
  85. </div>
  86. </div>
  87. </div>
  88. <Button type="link" class="template-select-trigger" @click="openTemplateModal">选择模板</Button>
  89. </div>
  90. <template v-if="hasTemplateSelected">
  91. <div class="middle-panel-body">
  92. <div class="mapping-table-card">
  93. <div class="mapping-card-head">
  94. <div class="mapping-card-title">字段映射</div>
  95. <div class="mapping-card-actions">
  96. <Button size="small" @click="() => autoMatchFields()">智能预匹配</Button>
  97. <Button size="small" @click="clearFieldMapping">清空映射</Button>
  98. </div>
  99. </div>
  100. <div class="mapping-table">
  101. <div class="mapping-head">
  102. <span>序号</span>
  103. <span>状态</span>
  104. <span>目标字段</span>
  105. <span>原始字段</span>
  106. </div>
  107. <div
  108. class="mapping-row"
  109. :class="{ 'is-active': activeMappingTargetKey === item.key }"
  110. v-for="(item, index) in sortedMappingTargetFields"
  111. :key="item.key"
  112. @click="handleMappingRowSelect(item.key)"
  113. >
  114. <span>{{ index + 1 }}</span>
  115. <span class="status-cell">
  116. <span class="mapping-status-group">
  117. <span class="mapping-status-badge" :class="getMappingStatusClass(item)">
  118. <span v-if="getMappingStatusClass(item) !== 'is-optional'" class="mapping-status-dot"></span>
  119. </span>
  120. <span
  121. v-if="hasTargetFormatError(item.key)"
  122. class="mapping-status-error"
  123. :title="getTargetFormatErrorTitle(item.key) || undefined"
  124. >
  125. !
  126. </span>
  127. </span>
  128. </span>
  129. <span class="target-field-cell">
  130. <span v-if="isMappingTargetRequired(item)" class="required">*</span>
  131. {{ item.label }}
  132. </span>
  133. <span class="origin-field-cell">
  134. <Popover
  135. trigger="click"
  136. placement="bottomRight"
  137. overlayClassName="origin-field-popover"
  138. :open="!isTargetHandledByRecognitionRule(item.key) && mappingPopoverOpenKey === item.key"
  139. @openChange="(open) => handleMappingPopoverOpen(item.key, open)"
  140. >
  141. <template #content>
  142. <div class="origin-pop">
  143. <div class="origin-pop-title">
  144. <span class="small-title">原始字段选择</span>
  145. <Input
  146. v-model:value="originFilterKeyword"
  147. size="small"
  148. placeholder="请输入搜索的字段名称"
  149. allow-clear
  150. />
  151. </div>
  152. <div
  153. class="origin-pop-list"
  154. :data-origin-list-target-key="item.key"
  155. @mouseleave="handleOriginOptionHoverLeave(item.key)"
  156. >
  157. <div
  158. class="origin-tag blank"
  159. @mouseenter="handleOriginOptionHoverLeave(item.key)"
  160. @click="setFieldBlank(item.key)"
  161. >
  162. 置空
  163. </div>
  164. <div
  165. v-for="opt in filteredOriginalOptions"
  166. :key="opt.value"
  167. class="origin-tag-wrap"
  168. @mouseenter="handleOriginOptionHover(item.key, opt.value)"
  169. >
  170. <div
  171. class="origin-tag"
  172. :data-origin-target-key="item.key"
  173. :data-origin-option-value="opt.value"
  174. :class="{
  175. disabled: isFieldTakenByOther(opt.value, item.key),
  176. active:
  177. getDisplayedMappedHeader(item.key) === opt.value ||
  178. isOriginOptionHovering(item.key, opt.value),
  179. }"
  180. @click="selectOriginalField(item.key, opt.value)"
  181. >
  182. {{ opt.label }}
  183. </div>
  184. <span
  185. v-if="isFieldTakenByOther(opt.value, item.key)"
  186. class="replace"
  187. @click="replaceOriginalField(item.key, opt.value)"
  188. >替换</span
  189. >
  190. </div>
  191. </div>
  192. <div class="origin-pop-bottom">+添加自定义表头</div>
  193. </div>
  194. </template>
  195. <div
  196. class="mapping-field-trigger"
  197. :class="{ 'is-disabled': isTargetHandledByRecognitionRule(item.key) }"
  198. @click="handleMappingTriggerClick(item.key)"
  199. >
  200. <span
  201. class="mapping-field-value"
  202. :class="{
  203. placeholder:
  204. !getDisplayedMappedHeader(item.key) && !isTargetHandledByRecognitionRule(item.key),
  205. }"
  206. >
  207. {{
  208. isTargetHandledByRecognitionRule(item.key)
  209. ? ''
  210. : getDisplayedMappedHeader(item.key) || '请选择原始字段'
  211. }}
  212. </span>
  213. <span v-if="!isTargetHandledByRecognitionRule(item.key)" class="mapping-field-arrow">▾</span>
  214. </div>
  215. </Popover>
  216. </span>
  217. </div>
  218. <div v-if="!fixedTargetFields.length" class="mapping-empty">当前模板暂无可映射字段</div>
  219. </div>
  220. </div>
  221. <div
  222. class="template-foot-panel"
  223. :class="{
  224. 'is-collapsed': showRecognitionPanel && !recognitionPanelExpanded,
  225. 'is-compact': !showRecognitionPanel,
  226. }"
  227. >
  228. <div v-if="showRecognitionPanel" class="recognition-panel">
  229. <div class="recognition-panel-head">
  230. <span class="recognition-title">识别规则设置</span>
  231. <span
  232. class="recognition-toggle"
  233. :class="{ expanded: recognitionPanelExpanded }"
  234. @click="recognitionPanelExpanded = !recognitionPanelExpanded"
  235. >
  236. {{ recognitionPanelExpanded ? '收起' : '展开' }}
  237. <component
  238. :is="recognitionPanelExpanded ? DownOutlined : UpOutlined"
  239. class="recognition-toggle-icon"
  240. />
  241. </span>
  242. </div>
  243. <div v-if="recognitionPanelExpanded" class="recognition-panel-body">
  244. <div class="recognition-tabs">
  245. <span
  246. v-for="item in recognitionRuleTabs"
  247. :key="item"
  248. class="recognition-tab"
  249. :class="{ active: activeRecognitionRule === item }"
  250. @click="activeRecognitionRule = item"
  251. >
  252. {{ item }}
  253. </span>
  254. </div>
  255. <div v-if="activeRecognitionRule === '基本信息配置'" class="recognition-basic-shell">
  256. <div class="recognition-basic-preview">
  257. <div class="recognition-basic-preview__head">
  258. <span class="recognition-basic-preview__title">表头上方内容</span>
  259. <span class="recognition-basic-preview__meta">{{ recognitionHeaderPreviewMeta }}</span>
  260. </div>
  261. <div class="recognition-basic-preview__content">{{ recognitionHeaderPreviewText }}</div>
  262. </div>
  263. <div v-if="showRecognitionBasicInfoPanel" class="recognition-basic-panel">
  264. <div class="recognition-basic-panel__head">
  265. <div class="recognition-basic-panel__summary">
  266. <div class="recognition-basic-panel__title-wrap">
  267. <span class="recognition-basic-panel__title">合并表头配置内容</span>
  268. <span class="recognition-basic-panel__count">{{ recognitionBasicInfoRows.length }} 项</span>
  269. </div>
  270. <div class="recognition-basic-panel__desc">
  271. 跟随模板中的合并表逻辑,为表头上方内容补充原始匹配项、目标字段与截止标志,目标字段保持模板默认映射。
  272. </div>
  273. </div>
  274. <div class="recognition-basic-panel__actions">
  275. <Button
  276. type="link"
  277. class="recognition-basic-panel__toggle-btn"
  278. @click.stop="recognitionBasicInfoExpanded = !recognitionBasicInfoExpanded"
  279. >
  280. {{ recognitionBasicInfoExpanded ? '收起' : '展开' }}
  281. </Button>
  282. </div>
  283. </div>
  284. <template v-if="recognitionBasicInfoExpanded">
  285. <div v-if="recognitionBasicInfoRows.length" class="recognition-basic-list">
  286. <div
  287. v-for="(item, index) in recognitionBasicInfoRows"
  288. :key="item.key"
  289. class="recognition-basic-item"
  290. >
  291. <div class="recognition-basic-item__index">{{ String(index + 1).padStart(2, '0') }}</div>
  292. <div class="recognition-basic-item__content">
  293. <div class="recognition-basic-field">
  294. <span class="recognition-basic-field__label">原始表头匹配项</span>
  295. <Input v-model:value="item.headerMatch" placeholder="请输入匹配项" />
  296. </div>
  297. <div class="recognition-basic-field">
  298. <span class="recognition-basic-field__label">目标字段</span>
  299. <Tooltip :title="item.targetField || undefined" placement="topLeft">
  300. <div
  301. class="recognition-basic-field__readonly"
  302. :class="{ 'is-empty': !item.targetField }"
  303. >
  304. {{ item.targetField || '未配置目标字段' }}
  305. </div>
  306. </Tooltip>
  307. </div>
  308. <div class="recognition-basic-field">
  309. <span class="recognition-basic-field__label">内容获取截止标志</span>
  310. <Input v-model:value="item.endFlag" placeholder="请输入截止标志" />
  311. </div>
  312. </div>
  313. </div>
  314. </div>
  315. <div v-else class="recognition-basic-empty">
  316. <div class="recognition-basic-empty__title">暂无合并配置</div>
  317. <div class="recognition-basic-empty__desc">
  318. 当前模板未解析到可展示的合并规则,请检查默认提取规则配置。
  319. </div>
  320. </div>
  321. </template>
  322. </div>
  323. <div v-else class="recognition-basic-empty-shell">
  324. <div class="recognition-basic-empty-shell__title">当前模板暂无合并表配置</div>
  325. <div class="recognition-basic-empty-shell__desc">
  326. 当前工作表会沿用模板默认合并表配置;若需调整,请先在模板库中维护对应的默认规则。
  327. </div>
  328. </div>
  329. </div>
  330. <div v-else-if="activeRecognitionRule === '正负标识'" class="recognition-zf-shell">
  331. <div class="recognition-zf-card">
  332. <div class="recognition-zf-card__main">
  333. <div class="recognition-zf-card__title">交易金额正负标识</div>
  334. <div class="recognition-zf-card__desc">
  335. 开启后,系统会依据交易金额的正负值识别收入/支出,借贷标志字段无需再单独映射原始列。
  336. </div>
  337. </div>
  338. <Switch v-model:checked="recognitionAmountZfEnabled" @change="handleRecognitionAmountZfChange" />
  339. </div>
  340. </div>
  341. <div
  342. v-else-if="activeRecognitionRule === '借贷标志' || activeRecognitionRule === '通信类别'"
  343. class="recognition-direction-shell"
  344. >
  345. <div class="recognition-direction-card-list">
  346. <div
  347. v-for="field in activeRecognitionDirectionFields"
  348. :key="field.key"
  349. class="recognition-direction-card"
  350. >
  351. <div class="recognition-direction-card__head">
  352. <span class="recognition-direction-card__title">{{ field.label }}</span>
  353. <span class="recognition-direction-card__tag">{{ field.entries.length }} 项</span>
  354. </div>
  355. <div class="recognition-direction-list">
  356. <div
  357. v-for="directionItem in field.entries"
  358. :key="directionItem.key"
  359. class="recognition-direction-row"
  360. >
  361. <span class="recognition-direction-label">{{ directionItem.label }}</span>
  362. <Tooltip :title="directionItem.value || undefined" placement="topLeft">
  363. <Input v-model:value="directionItem.value" placeholder="请输入识别关键词" />
  364. </Tooltip>
  365. </div>
  366. </div>
  367. </div>
  368. </div>
  369. <div class="recognition-direction-actions">
  370. <Button @click="handleClearRecognitionDirection">清空规则</Button>
  371. <Button type="primary" @click="handleSaveRecognitionDirection">保存</Button>
  372. </div>
  373. </div>
  374. <div v-else class="recognition-rule-placeholder">
  375. <div class="recognition-rule-placeholder__title">{{ activeRecognitionRule }}</div>
  376. <div class="recognition-rule-placeholder__desc">
  377. 当前规则仍沿用已配置的模板字段与右侧清洗逻辑,后续可继续补充专项识别参数。
  378. </div>
  379. </div>
  380. </div>
  381. </div>
  382. <div class="child-template-panel">
  383. <div class="child-template-tip">
  384. 子模板输入名称,导入数据会自动保存。下次上传同样的数据子模板会自动识别。
  385. </div>
  386. <div class="child-template-form">
  387. <span class="child-template-label">创建子模板:</span>
  388. <Input
  389. v-model:value="childTemplateName"
  390. placeholder="请输入子模板名称"
  391. @blur="handleChildTemplateNameBlur"
  392. />
  393. </div>
  394. </div>
  395. </div>
  396. </div>
  397. </template>
  398. <div v-else class="middle-empty-shell">
  399. <div class="middle-empty-stage">
  400. <div class="middle-empty-stage-head">
  401. <span class="middle-empty-sheet">当前工作表:{{ activeSheetDisplayName }}</span>
  402. </div>
  403. <div class="middle-empty-stage-main">
  404. <div class="middle-empty-visual" aria-hidden="true">
  405. <span class="middle-empty-visual__halo is-large"></span>
  406. <span class="middle-empty-visual__halo is-small"></span>
  407. <span class="middle-empty-visual__card"></span>
  408. <span class="middle-empty-visual__card is-back"></span>
  409. <span class="middle-empty-visual__dot"></span>
  410. </div>
  411. <div class="middle-empty-actions">
  412. <Button type="primary" @click="openTemplateModal">选择模板</Button>
  413. <span class="middle-empty-hint">支持从模板库实时加载模板,并同步初始化字段映射区。</span>
  414. </div>
  415. </div>
  416. </div>
  417. <div class="middle-empty-highlights">
  418. <div class="middle-empty-card">
  419. <span class="middle-empty-card__index">01</span>
  420. <div class="middle-empty-card__title">锁定字段结构</div>
  421. <div class="middle-empty-card__desc">先明确目标字段与必填项,再进入映射配置,减少重复调整。</div>
  422. </div>
  423. <div class="middle-empty-card">
  424. <span class="middle-empty-card__index">02</span>
  425. <div class="middle-empty-card__title">自动回填规则</div>
  426. <div class="middle-empty-card__desc">命中模板后可直接带出字段关系,并用当前表头继续补齐。</div>
  427. </div>
  428. <div class="middle-empty-card">
  429. <span class="middle-empty-card__index">03</span>
  430. <div class="middle-empty-card__title">沉淀子模板</div>
  431. <div class="middle-empty-card__desc">完成映射后可保存识别规则,方便同类文件后续自动识别。</div>
  432. </div>
  433. </div>
  434. </div>
  435. </section>
  436. <div class="panel-resizer" @mousedown="startResize('middle', $event)"></div>
  437. <section class="right-panel">
  438. <div class="panel-header">
  439. <div class="panel-title-wrap">
  440. <div class="panel-title">数据清洗区</div>
  441. </div>
  442. </div>
  443. <div class="toolbar toolbar--column-actions">
  444. <div class="toolbar-head">
  445. <span class="toolbar-label">列操作</span>
  446. </div>
  447. <div class="toolbar-actions">
  448. <Button size="small" type="primary" @click="openAddColumn">新增列</Button>
  449. <Button size="small" @click="openSplitColumn">拆分列</Button>
  450. <Button size="small" @click="openEditColumn">编辑列</Button>
  451. <Button size="small" @click="openDeleteColumn">删除列</Button>
  452. </div>
  453. <div class="toolbar-meta toolbar-meta--header-row">
  454. <span class="meta-label">表头行</span>
  455. <Input
  456. v-model:value="headerRowInput"
  457. size="small"
  458. class="meta-input"
  459. placeholder="输入行号"
  460. @pressEnter="applyHeaderRowInput"
  461. @blur="applyHeaderRowInput"
  462. />
  463. </div>
  464. </div>
  465. <div class="right-panel-body">
  466. <div ref="tableMockRef" class="table-mock">
  467. <div class="mock-head" :style="{ gridTemplateColumns: mockGridTemplate }">
  468. <span class="row-index-cell row-index-head">表头行</span>
  469. <span
  470. v-for="(header, colIndex) in rightTableHeaders"
  471. :key="header"
  472. :data-preview-col="colIndex"
  473. :class="{
  474. 'is-added': addedColumns.includes(header),
  475. active: selectedAddedColumn === header,
  476. 'is-current-col': currentColumn === colIndex,
  477. 'is-first-col': colIndex === 0,
  478. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  479. }"
  480. @click="onClickHeader(header)"
  481. >
  482. <span>{{ header }}</span>
  483. <span
  484. v-if="getColumnRuleBadgeState(colIndex)"
  485. class="rule-badge"
  486. :class="`is-${getColumnRuleBadgeState(colIndex)}`"
  487. :title="getColumnRuleBadgeTitle(colIndex)"
  488. ></span>
  489. </span>
  490. </div>
  491. <div class="mock-match-row" :style="{ gridTemplateColumns: mockGridTemplate }">
  492. <span class="row-index-cell match-row-index">目标字段</span>
  493. <span
  494. v-for="(header, colIndex) in rightTableHeaders"
  495. :key="`mapping-${header}`"
  496. :data-preview-col="colIndex"
  497. :class="{
  498. 'is-current-col': currentColumn === colIndex,
  499. 'is-first-col': colIndex === 0,
  500. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  501. 'is-empty': !getMatchedTargetFieldText(header),
  502. }"
  503. @click="onClickHeader(header)"
  504. >
  505. <span class="match-field-content">
  506. <span class="match-field-text">{{ getMatchedTargetFieldText(header) || '-' }}</span>
  507. <span class="match-status-group">
  508. <span
  509. class="match-status-badge"
  510. :class="getMatchedHeaderStatusClass(header)"
  511. :title="getMatchedHeaderStatusTitle(header) || undefined"
  512. >
  513. {{ getMatchedHeaderStatusText(header) }}
  514. </span>
  515. <span
  516. v-if="hasHeaderFormatError(header)"
  517. class="match-status-badge is-formatError"
  518. :title="getHeaderFormatErrorTitle(header) || undefined"
  519. >
  520. !
  521. </span>
  522. </span>
  523. </span>
  524. </span>
  525. </div>
  526. <div
  527. class="mock-row"
  528. :style="{ gridTemplateColumns: mockGridTemplate }"
  529. :class="{
  530. 'is-ignored-row': ignoredRowsSet.has(rowIndex + 1),
  531. 'is-current-row': selectedRowIndex === rowIndex,
  532. 'is-header-row': headerRowIndex === rowIndex,
  533. }"
  534. v-for="(row, rowIndex) in tablePreviewRows"
  535. :key="`row-${rowIndex}`"
  536. >
  537. <span class="row-index-cell" @click="selectedRowIndex = rowIndex">
  538. {{ rowIndex + 1 }}
  539. </span>
  540. <span
  541. v-for="(header, colIndex) in rightTableHeaders"
  542. :key="`${header}-${rowIndex}`"
  543. :data-preview-col="colIndex"
  544. :class="{
  545. 'is-current-col': currentColumn === colIndex,
  546. 'is-first-col': colIndex === 0,
  547. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  548. }"
  549. @click="onClickCell(rowIndex, header, colIndex)"
  550. >
  551. {{ row[header] || '' }}
  552. </span>
  553. </div>
  554. </div>
  555. <div class="cleaning-rule-panel" :class="{ 'is-collapsed': !cleaningRulePanelExpanded }">
  556. <div class="rule-title-row">
  557. <div class="rule-tabs">
  558. <span
  559. v-for="(tab, idx) in rulesList"
  560. :key="tab.name"
  561. class="rule-tab"
  562. :class="{ active: activeRules === idx }"
  563. @click="rulesListCheck(idx)"
  564. >
  565. {{ tab.name }}
  566. </span>
  567. </div>
  568. <div class="rule-title-meta">
  569. <span class="current-col">当前选中列:{{ currentColumnTitle || '-' }}</span>
  570. <span
  571. class="rule-toggle"
  572. :class="{ expanded: cleaningRulePanelExpanded }"
  573. @click="cleaningRulePanelExpanded = !cleaningRulePanelExpanded"
  574. >
  575. {{ cleaningRulePanelExpanded ? '收起' : '展开' }}
  576. <component :is="cleaningRulePanelExpanded ? DownOutlined : UpOutlined" class="rule-toggle-icon" />
  577. </span>
  578. </div>
  579. </div>
  580. <div v-show="cleaningRulePanelExpanded" class="rule-content">
  581. <div class="rule-col">
  582. <div class="col-title">清洗规则</div>
  583. <div class="col-list">
  584. <div
  585. v-for="(item, idx) in visibleCleaningRules"
  586. :key="`${item.key}-${idx}`"
  587. class="col-item"
  588. :class="{ active: item.active }"
  589. @click="rulesListClick(item.key)"
  590. @dblclick="dbRulesListClick(item.key)"
  591. >
  592. {{ item.name }}
  593. </div>
  594. </div>
  595. </div>
  596. <div class="rule-op">
  597. <Button size="small" @click="rulesListRight">&gt;</Button>
  598. <Button size="small" @click="rulesListLeft">&lt;</Button>
  599. </div>
  600. <div class="rule-col">
  601. <div class="col-title between">
  602. <span>已选规则</span>
  603. <span class="clear-btn" @click="selectRulesDel">全部删除</span>
  604. </div>
  605. <div class="col-list">
  606. <div
  607. v-for="(item, idx) in currentSelectedRules"
  608. :key="`${item.key}-${idx}`"
  609. class="col-item"
  610. :class="{ active: item.active }"
  611. @click="selectRulesClick(idx)"
  612. >
  613. <span class="col-item-label">{{ item.name }}</span>
  614. <span class="col-item-status" :class="{ 'is-enabled': item.enabled, 'is-disabled': !item.enabled }">
  615. {{ item.enabled ? '已启用' : '未启用' }}
  616. </span>
  617. </div>
  618. </div>
  619. </div>
  620. <div class="rule-config">
  621. <div class="col-title">{{
  622. currentActiveSelectedRule || !currentRuleExample ? '规则配置' : '示例'
  623. }}</div>
  624. <div class="config-placeholder">
  625. <template v-if="currentActiveSelectedRule">
  626. <div class="config-rule-shell">
  627. <div class="config-rule-head">
  628. <div class="config-rule-title-row">
  629. <div class="config-rule-copy">
  630. <div class="config-rule-title">
  631. {{ currentRuleTitle || ruleNameMap[currentActiveSelectedRule.key] }}
  632. </div>
  633. </div>
  634. <div class="config-rule-meta">
  635. <span class="config-rule-state" :class="currentRuleEnabledClass">
  636. {{ currentRuleEnabledLabel }}
  637. </span>
  638. <span class="config-rule-tag" :class="`is-${currentRuleScopeType}`">
  639. {{ currentRuleScopeLabel }}
  640. </span>
  641. <Button
  642. v-if="currentActiveSelectedRule.enabled"
  643. size="small"
  644. class="config-rule-toggle-btn"
  645. @click="toggleCurrentRuleEnabled(false)"
  646. >
  647. 停用规则
  648. </Button>
  649. </div>
  650. </div>
  651. </div>
  652. <template v-if="currentActiveSelectedRule.key === 'ignoreLine'">
  653. <div class="config-section">
  654. <div class="config-section-title">参数配置</div>
  655. <div class="config-field">
  656. <span class="config-field-label">忽略行号</span>
  657. <Input v-model:value="omitLineInput" placeholder="例如:2,5,8" />
  658. <div class="config-hint">
  659. 按 1 开始计数,多个行号使用英文逗号分隔,预览结果和左侧结构会同步忽略这些行。
  660. </div>
  661. </div>
  662. <div class="config-actions">
  663. <Button type="primary" size="small" @click="applyIgnoreLineRule">应用规则</Button>
  664. </div>
  665. </div>
  666. <div class="config-section is-example">
  667. <div class="config-section-title">规则示例</div>
  668. <div class="config-example-list">
  669. <div class="config-example-row">
  670. <span>输入</span>
  671. <code>2,5,8</code>
  672. </div>
  673. <div class="config-example-row">
  674. <span>效果</span>
  675. <span>第 2、5、8 行不会参与后续清洗与导入</span>
  676. </div>
  677. </div>
  678. </div>
  679. </template>
  680. <template v-else-if="currentActiveSelectedRule.key === 'dateConfig'">
  681. <div class="config-section">
  682. <div class="config-section-title">参数配置</div>
  683. <div class="config-field">
  684. <span class="config-field-label">输出类型</span>
  685. <Select v-model:value="dateConfigType" :options="dateTypeOptions" />
  686. <div class="config-hint">
  687. 根据源值自动识别常见日期格式,并统一转换为标准日期文本输出。
  688. </div>
  689. </div>
  690. <div class="config-actions">
  691. <Button type="primary" size="small" @click="applyDateConfigRule">应用规则</Button>
  692. </div>
  693. </div>
  694. <div class="config-section is-example">
  695. <div class="config-section-title">规则示例</div>
  696. <div class="config-example-list">
  697. <div class="config-example-row">
  698. <span>原始值</span>
  699. <code>20260324153020</code>
  700. </div>
  701. <div class="config-example-row">
  702. <span>日期时间</span>
  703. <code>2026-03-24 15:30:20</code>
  704. </div>
  705. <div class="config-example-row">
  706. <span>仅日期</span>
  707. <code>2026-03-24</code>
  708. </div>
  709. </div>
  710. </div>
  711. </template>
  712. <template v-else-if="currentActiveSelectedRule.key === 'extractData'">
  713. <div class="config-section">
  714. <div class="config-section-title">参数配置</div>
  715. <Radio.Group v-model:value="extractDataMode" class="config-extract-data-group">
  716. <div class="config-extract-data-item">
  717. <Radio value="before" @change="handleExtractDataModeChange('before')">提取前</Radio>
  718. <Input
  719. v-model:value="extractDataBefore"
  720. :disabled="extractDataMode !== 'before'"
  721. placeholder="例如:6"
  722. />
  723. <span class="config-extract-data-unit">位</span>
  724. </div>
  725. <div class="config-extract-data-item">
  726. <Radio value="after" @change="handleExtractDataModeChange('after')">提取后</Radio>
  727. <Input
  728. v-model:value="extractDataAfter"
  729. :disabled="extractDataMode !== 'after'"
  730. placeholder="例如:4"
  731. />
  732. <span class="config-extract-data-unit">位</span>
  733. </div>
  734. </Radio.Group>
  735. <div class="config-hint">提取前与提取后互斥,只会应用当前选中的提取方向。</div>
  736. <div class="config-actions">
  737. <Button type="primary" size="small" @click="applyExtractDataRule">应用规则</Button>
  738. </div>
  739. </div>
  740. <div class="config-section is-example">
  741. <div class="config-section-title">规则示例</div>
  742. <div class="config-example-list">
  743. <div class="config-example-row">
  744. <span>原始值</span>
  745. <code>13800138000</code>
  746. </div>
  747. <div class="config-example-row">
  748. <span>前 3 位</span>
  749. <code>138</code>
  750. </div>
  751. <div class="config-example-row">
  752. <span>后 4 位</span>
  753. <code>8000</code>
  754. </div>
  755. </div>
  756. </div>
  757. </template>
  758. <template v-else-if="currentActiveSelectedRule.key === 'extractSegment'">
  759. <div class="config-section">
  760. <div class="config-section-title">参数配置</div>
  761. <div class="config-form-grid">
  762. <div class="config-field">
  763. <span class="config-field-label">分隔符</span>
  764. <Input v-model:value="extractSegmentValue" placeholder="例如:-" />
  765. </div>
  766. <div class="config-field">
  767. <span class="config-field-label">提取段位</span>
  768. <InputNumber v-model:value="extractSegmentRow" :min="1" :precision="0" />
  769. </div>
  770. </div>
  771. <div class="config-hint">按分隔符拆分后,从左到右提取指定段位内容。</div>
  772. <div class="config-actions">
  773. <Button type="primary" size="small" @click="applyExtractSegmentRule">应用规则</Button>
  774. </div>
  775. </div>
  776. <div class="config-section is-example">
  777. <div class="config-section-title">规则示例</div>
  778. <div class="config-example-list">
  779. <div class="config-example-row">
  780. <span>原始值</span>
  781. <code>四川省-成都市-高新区</code>
  782. </div>
  783. <div class="config-example-row">
  784. <span>分隔符</span>
  785. <code>-</code>
  786. </div>
  787. <div class="config-example-row">
  788. <span>第 2 段</span>
  789. <code>成都市</code>
  790. </div>
  791. </div>
  792. </div>
  793. </template>
  794. <template v-else-if="currentActiveSelectedRule.key === 'timestampConversion'">
  795. <div class="config-section">
  796. <div class="config-section-title">规则说明</div>
  797. <div class="config-desc-list">
  798. <div class="config-desc-item">自动识别 10 位或 13 位时间戳,并转换为标准时间文本。</div>
  799. <div class="config-desc-item">适用于毫秒时间戳、秒时间戳以及常见纯数字时间字段。</div>
  800. </div>
  801. <div class="config-actions">
  802. <Button type="primary" size="small" @click="applyTimestampConversionRule">确认启用</Button>
  803. </div>
  804. </div>
  805. <div class="config-section is-example">
  806. <div class="config-section-title">规则示例</div>
  807. <div class="config-example-list">
  808. <div class="config-example-row">
  809. <span>时间戳</span>
  810. <code>1711209600</code>
  811. </div>
  812. <div class="config-example-row">
  813. <span>转换后</span>
  814. <code>2024-03-24 00:00:00</code>
  815. </div>
  816. </div>
  817. </div>
  818. </template>
  819. <template v-else-if="currentActiveSelectedRule.key === 'removeSpecialCharacters'">
  820. <div class="config-section">
  821. <div class="config-section-title">参数配置</div>
  822. <div class="config-check-list">
  823. <Checkbox v-model:checked="removeSpaceChecked">移除空格字符</Checkbox>
  824. <Checkbox v-model:checked="removeTabChecked">移除换行字符(tab)</Checkbox>
  825. </div>
  826. <div class="config-field">
  827. <span class="config-field-label">自定义移除字符</span>
  828. <div class="config-check-inline">
  829. <Checkbox v-model:checked="removeCustomChecked">启用</Checkbox>
  830. <Input
  831. v-model:value="removeCustomText"
  832. :disabled="!removeCustomChecked"
  833. placeholder="例如:-"
  834. />
  835. </div>
  836. </div>
  837. <div class="config-actions">
  838. <Button type="primary" size="small" @click="applyRemoveSpecialRule">应用规则</Button>
  839. </div>
  840. </div>
  841. <div class="config-section is-example">
  842. <div class="config-section-title">规则示例</div>
  843. <div class="config-example-list">
  844. <div class="config-example-row">
  845. <span>原始值</span>
  846. <code>138 0013-8000</code>
  847. </div>
  848. <div class="config-example-row">
  849. <span>清洗后</span>
  850. <code>13800138000</code>
  851. </div>
  852. </div>
  853. </div>
  854. </template>
  855. <template v-else-if="currentActiveSelectedRule.key === 'absoluteValue'">
  856. <div class="config-section">
  857. <div class="config-section-title">参数配置</div>
  858. <div class="config-check-list single">
  859. <Checkbox v-model:checked="absoluteValueChecked">将当前列转换为绝对值</Checkbox>
  860. </div>
  861. <div class="config-hint">仅对数值型内容生效,负数会转换为对应正值。</div>
  862. <div class="config-actions">
  863. <Button type="primary" size="small" @click="applyAbsoluteValueRule">应用规则</Button>
  864. </div>
  865. </div>
  866. <div class="config-section is-example">
  867. <div class="config-section-title">规则示例</div>
  868. <div class="config-example-list">
  869. <div class="config-example-row">
  870. <span>原始值</span>
  871. <code>-20</code>
  872. </div>
  873. <div class="config-example-row">
  874. <span>清洗后</span>
  875. <code>20</code>
  876. </div>
  877. </div>
  878. </div>
  879. </template>
  880. <template v-else-if="currentActiveSelectedRule.key === 'decimalConversion'">
  881. <div class="config-section">
  882. <div class="config-section-title">参数配置</div>
  883. <div class="config-check-list single">
  884. <Checkbox v-model:checked="decimalEnabled">启用进制转换</Checkbox>
  885. </div>
  886. <div class="config-form-grid">
  887. <div class="config-field">
  888. <span class="config-field-label">当前进制</span>
  889. <Select v-model:value="decimalSourceBase" :options="decimalBaseOptions" />
  890. </div>
  891. <div class="config-field">
  892. <span class="config-field-label">转换后进制</span>
  893. <Select v-model:value="decimalTargetBase" :options="decimalBaseOptions" />
  894. </div>
  895. </div>
  896. <div class="config-actions">
  897. <Button type="primary" size="small" @click="applyDecimalConversionRule">应用规则</Button>
  898. </div>
  899. </div>
  900. <div class="config-section is-example">
  901. <div class="config-section-title">规则示例</div>
  902. <div class="config-example-list">
  903. <div class="config-example-row">
  904. <span>原始值</span>
  905. <code>56(10进制)</code>
  906. </div>
  907. <div class="config-example-row">
  908. <span>清洗后</span>
  909. <code>38(16进制)</code>
  910. </div>
  911. </div>
  912. </div>
  913. </template>
  914. <template v-else-if="currentActiveSelectedRule.key === 'replaceValue'">
  915. <div class="config-section">
  916. <div class="config-section-title">参数配置</div>
  917. <div class="config-check-list single">
  918. <Checkbox v-model:checked="replaceEnabled">启用条件替换</Checkbox>
  919. </div>
  920. <div class="config-field">
  921. <span class="config-field-label">匹配方式</span>
  922. <Radio.Group v-model:value="replaceMode" class="config-radio-group">
  923. <Radio v-for="item in replaceModeOptions" :key="item.value" :value="item.value">
  924. {{ item.label }}
  925. </Radio>
  926. </Radio.Group>
  927. </div>
  928. <div class="config-form-grid">
  929. <div class="config-field">
  930. <span class="config-field-label">源值</span>
  931. <Input v-model:value="replaceSource" placeholder="例如:未知" />
  932. </div>
  933. <div class="config-field">
  934. <span class="config-field-label">目标值</span>
  935. <Input v-model:value="replaceTarget" placeholder="例如:--" />
  936. </div>
  937. </div>
  938. <div class="config-actions">
  939. <Button type="primary" size="small" @click="applyReplaceRule">应用规则</Button>
  940. </div>
  941. </div>
  942. <div class="config-section is-example">
  943. <div class="config-section-title">规则示例</div>
  944. <div class="config-example-list">
  945. <div class="config-example-row">
  946. <span>包含“未知”</span>
  947. <code>未知开户地址 -&gt; --开户地址</code>
  948. </div>
  949. <div class="config-example-row">
  950. <span>等于“空”</span>
  951. <code>空 -&gt; --</code>
  952. </div>
  953. </div>
  954. </div>
  955. </template>
  956. <template v-else-if="currentActiveSelectedRule.key === 'groupingColumns'">
  957. <div class="config-section">
  958. <div class="config-section-title">参数配置</div>
  959. <div class="config-hint">
  960. 仅建议用于新增列。按配置顺序拼接多个来源列的前/后 N 位内容,点击“应用规则”后生效。
  961. </div>
  962. <div v-if="!isCurrentColumnAdded" class="config-alert">
  963. 当前列不是新增列,组合列规则通常用于新增列结果字段。
  964. </div>
  965. <div class="grouping-list">
  966. <div class="grouping-row" v-for="(item, idx) in groupingColumnsDraft" :key="`group-${idx}`">
  967. <div class="config-field">
  968. <span class="config-field-label">来源表头</span>
  969. <Select
  970. v-model:value="item.header"
  971. :options="groupingHeaderOptions"
  972. placeholder="请选择表头"
  973. @change="(value) => handleGroupingColumnHeaderChange(idx, String(value || ''))"
  974. />
  975. </div>
  976. <div class="config-field">
  977. <span class="config-field-label">提取方向</span>
  978. <Radio.Group v-model:value="item.direction" class="config-radio-group compact">
  979. <Radio value="前">前</Radio>
  980. <Radio value="后">后</Radio>
  981. </Radio.Group>
  982. </div>
  983. <div class="config-field">
  984. <span class="config-field-label">位数</span>
  985. <InputNumber v-model:value="item.count" :min="1" :precision="0" />
  986. </div>
  987. <Button size="small" class="grouping-remove-btn" @click="removeGroupingColumnDraft(idx)">
  988. 删除
  989. </Button>
  990. </div>
  991. </div>
  992. <div class="config-actions between">
  993. <Button size="small" @click="addGroupingColumnDraft">新增一行</Button>
  994. <Button type="primary" size="small" @click="applyGroupingColumnsRule">应用规则</Button>
  995. </div>
  996. </div>
  997. <div class="config-section is-example">
  998. <div class="config-section-title">规则示例</div>
  999. <div class="config-example-list">
  1000. <div class="config-example-row">
  1001. <span>配置方式</span>
  1002. <code>姓名前 1 位 + 手机号后 4 位</code>
  1003. </div>
  1004. <div class="config-example-row">
  1005. <span>组合结果</span>
  1006. <code>张2369</code>
  1007. </div>
  1008. </div>
  1009. </div>
  1010. </template>
  1011. <template v-else-if="currentActiveSelectedRule.key === 'magnificationAdjustment'">
  1012. <div class="config-section">
  1013. <div class="config-section-title">参数配置</div>
  1014. <div class="config-field">
  1015. <span class="config-field-label">放大倍数</span>
  1016. <InputNumber v-model:value="magnificationValue" :min="1" />
  1017. <div class="config-hint">
  1018. 当前规则会按照“原值 × 倍数”输出,适合金额或比例类字段统一换算。
  1019. </div>
  1020. </div>
  1021. <div class="config-actions">
  1022. <Button type="primary" size="small" @click="applyMagnificationRule">应用规则</Button>
  1023. </div>
  1024. </div>
  1025. <div class="config-section is-example">
  1026. <div class="config-section-title">规则示例</div>
  1027. <div class="config-example-list">
  1028. <div class="config-example-row">
  1029. <span>原始值</span>
  1030. <code>1.23</code>
  1031. </div>
  1032. <div class="config-example-row">
  1033. <span>倍数 100</span>
  1034. <code>123</code>
  1035. </div>
  1036. </div>
  1037. </div>
  1038. </template>
  1039. <template v-else-if="currentActiveSelectedRule.key === 'yearComplete'">
  1040. <div class="config-section">
  1041. <div class="config-section-title">规则说明</div>
  1042. <div class="config-desc-list">
  1043. <div class="config-desc-item">当日期值缺少年份时,系统会按当前年份自动补全年份。</div>
  1044. <div class="config-desc-item">若补全年份后的日期晚于当前时间,则会自动回退到上一年。</div>
  1045. </div>
  1046. <div class="config-actions">
  1047. <Button type="primary" size="small" @click="applyYearCompleteRule">确认启用</Button>
  1048. </div>
  1049. </div>
  1050. <div class="config-section is-example">
  1051. <div class="config-section-title">规则示例</div>
  1052. <div class="config-example-list">
  1053. <div class="config-example-row">
  1054. <span>原始值</span>
  1055. <code>02-23 12:00:00</code>
  1056. </div>
  1057. <div class="config-example-row">
  1058. <span>补全年份后</span>
  1059. <code>2026-02-23 12:00:00</code>
  1060. </div>
  1061. </div>
  1062. </div>
  1063. </template>
  1064. </div>
  1065. </template>
  1066. <template v-else>
  1067. <div v-if="currentRuleExample" class="config-rule-shell is-example-preview">
  1068. <div class="config-rule-head">
  1069. <div class="config-rule-title-row">
  1070. <div class="config-rule-copy">
  1071. <div class="config-rule-title">{{ currentRuleExample.title }}</div>
  1072. <div class="config-rule-desc">{{ currentRuleExample.description }}</div>
  1073. </div>
  1074. <div class="config-rule-meta">
  1075. <span class="config-rule-state is-example">示例</span>
  1076. <span class="config-rule-tag" :class="`is-${currentRuleScopeType}`">
  1077. {{ currentRuleScopeLabel }}
  1078. </span>
  1079. </div>
  1080. </div>
  1081. </div>
  1082. <div class="config-section is-example">
  1083. <div class="config-section-title">规则示例</div>
  1084. <div class="config-example-list">
  1085. <div v-for="row in currentRuleExample.rows" :key="row.label" class="config-example-row">
  1086. <span>{{ row.label }}</span>
  1087. <code>{{ row.value }}</code>
  1088. </div>
  1089. </div>
  1090. </div>
  1091. <div class="config-preview-tip">
  1092. 双击左侧规则或点击“&gt;”添加到“已选规则”后,可配置参数并应用到当前列。
  1093. </div>
  1094. </div>
  1095. <div v-else class="config-empty-state">
  1096. <div class="empty-title">规则配置说明</div>
  1097. <div class="empty-text">
  1098. 当前规则:{{ currentRuleTitle || '未选择' }}({{ currentActiveRuleDisplayName }})
  1099. </div>
  1100. <div class="empty-tip">请先从左侧选择规则并添加到“已选规则”,再在此处进行参数配置。</div>
  1101. </div>
  1102. </template>
  1103. </div>
  1104. </div>
  1105. </div>
  1106. </div>
  1107. </div>
  1108. </section>
  1109. </div>
  1110. <Modal
  1111. v-model:open="addColumnDialogOpen"
  1112. :footer="null"
  1113. :width="704"
  1114. :centered="true"
  1115. @cancel="resetAddColumnForm"
  1116. wrapClassName="cleaning-config-modal add-column-modal"
  1117. >
  1118. <template #title>
  1119. <div class="cleaning-modal-title">
  1120. <span class="title-main">{{ addColumnMode === 'add' ? '新增列' : '编辑列' }}</span>
  1121. <span class="title-desc">支持引用原始列或录入固定文本,快速生成新的目标列。</span>
  1122. </div>
  1123. </template>
  1124. <div class="add-col-dialog addColumn">
  1125. <div class="addColumn-name">
  1126. <div class="row">
  1127. <span class="label required">{{ addColumnMode === 'add' ? '新增列名' : '编辑列名' }}:</span>
  1128. <Input v-model:value="addColumnForm.name" class="name-input" placeholder="请输入列名" />
  1129. </div>
  1130. </div>
  1131. <div class="addColumn-tip">双击左侧原始数据列即可加入右侧新列内容,右侧支持上下调整拼接顺序。</div>
  1132. <div class="addColumn-content">
  1133. <div class="addColumn-content-left">
  1134. <div class="content-card-head">
  1135. <div class="content-left-tip">原始数据列名</div>
  1136. <div class="content-card-desc">从当前数据表中选择可引用字段</div>
  1137. </div>
  1138. <div class="left-oldColumn">
  1139. <div
  1140. v-for="field in sourceFieldCandidates"
  1141. :key="field"
  1142. class="oldColumn-box"
  1143. :title="field"
  1144. :class="{ 'active-item': selectedSourceField === field }"
  1145. @click="selectedSourceField = field"
  1146. @dblclick="appendSourceField(field)"
  1147. >
  1148. {{ field }}
  1149. </div>
  1150. </div>
  1151. </div>
  1152. <div class="addColumn-content-right">
  1153. <div class="right-header">
  1154. <div class="content-card-head">
  1155. <div class="content-right-tip">新列内容</div>
  1156. <div class="content-card-desc">
  1157. {{ addColumnForm.mode === 'ref' ? '支持拖动排序并删除单个字段' : '可直接输入固定文本作为新列内容' }}
  1158. </div>
  1159. </div>
  1160. </div>
  1161. <div class="right-radio">
  1162. <Radio.Group v-model:value="addColumnForm.mode">
  1163. <Radio value="ref">引用已有列</Radio>
  1164. <Radio value="custom">自定义列内容</Radio>
  1165. </Radio.Group>
  1166. </div>
  1167. <div class="right-newColumn" v-show="addColumnForm.mode === 'ref'">
  1168. <div
  1169. v-for="(field, index) in addColumnForm.contentFields"
  1170. :key="`${field}-${index}`"
  1171. class="newColumn-box"
  1172. :class="{
  1173. 'active-item': selectedContentIndex === index,
  1174. 'is-dragging': draggingContentIndex === index,
  1175. 'is-drag-over': dragOverContentIndex === index && draggingContentIndex !== index,
  1176. }"
  1177. draggable="true"
  1178. @click="selectedContentIndex = index"
  1179. @dragstart="handleContentDragStart(index, $event)"
  1180. @dragenter="handleContentDragEnter(index)"
  1181. @dragover="handleContentDragOver(index, $event)"
  1182. @drop="handleContentDrop(index, $event)"
  1183. @dragend="handleContentDragEnd"
  1184. >
  1185. <span class="newColumn-text" :title="field">{{ field }}</span>
  1186. <span class="del-icon" @click.stop="removeContentField(index)">×</span>
  1187. </div>
  1188. </div>
  1189. <div class="right-input" v-show="addColumnForm.mode === 'custom'">
  1190. <Input
  1191. v-model:value="addColumnForm.customContent"
  1192. :maxlength="100"
  1193. placeholder="请输入自定义列内容"
  1194. style="height: 32px; line-height: 32px"
  1195. />
  1196. </div>
  1197. </div>
  1198. </div>
  1199. <div class="dialog-footer">
  1200. <Button class="add-col-cancel" @click="resetAddColumnForm">取消</Button>
  1201. <Button type="primary" class="add-col-confirm" @click="confirmAddOrEditColumn">确定</Button>
  1202. </div>
  1203. </div>
  1204. </Modal>
  1205. <Modal
  1206. v-model:open="splitColumnDialogOpen"
  1207. :width="460"
  1208. :centered="true"
  1209. okText="确定"
  1210. cancelText="取消"
  1211. @ok="confirmSplitColumn"
  1212. @cancel="resetSplitForm"
  1213. wrapClassName="cleaning-config-modal split-column-modal"
  1214. >
  1215. <template #title>
  1216. <div class="cleaning-modal-title">
  1217. <span class="title-main">拆分列</span>
  1218. <span class="title-desc">按字符长度或特殊标识将单列拆分为两个目标列,适合姓名、地址等复合字段。</span>
  1219. </div>
  1220. </template>
  1221. <div class="split-column-form">
  1222. <div class="split-column-tip">选择需要拆分的原始列,并填写拆分后的列名与拆分截止标识。</div>
  1223. <div class="split-form-card">
  1224. <div class="form-row">
  1225. <span class="form-label">选择需要拆分的列:</span>
  1226. <Select
  1227. v-model:value="splitColumnForm.source"
  1228. :options="splitSourceOptions"
  1229. placeholder="请选择"
  1230. class="form-input"
  1231. />
  1232. </div>
  1233. <div class="form-row">
  1234. <span class="form-label required">拆分后列名1:</span>
  1235. <Input v-model:value="splitColumnForm.targetName1" placeholder="请输入" class="form-input" />
  1236. </div>
  1237. <div class="form-row">
  1238. <span class="form-label required">拆分后列名2:</span>
  1239. <Input v-model:value="splitColumnForm.targetName2" placeholder="请输入" class="form-input" />
  1240. </div>
  1241. <div class="form-row">
  1242. <span class="form-label required">拆分截止标识:</span>
  1243. <div class="delimiter-group">
  1244. <Select
  1245. v-model:value="splitColumnForm.delimiterType"
  1246. :options="delimiterOptions"
  1247. class="delimiter-select"
  1248. />
  1249. <Input
  1250. v-model:value="splitColumnForm.delimiterValue"
  1251. :placeholder="splitColumnForm.delimiterType === 'length' ? '请输入字符长度' : '请输入特殊标识'"
  1252. class="delimiter-input"
  1253. />
  1254. </div>
  1255. </div>
  1256. </div>
  1257. </div>
  1258. </Modal>
  1259. <Modal v-model:open="deleteColumnDialogOpen" title="删除列" :width="520" :centered="true" @ok="confirmDeleteColumn">
  1260. <div>确认删除列「{{ selectedAddedColumn || '-' }}」?</div>
  1261. </Modal>
  1262. <Modal
  1263. v-model:open="templateModalOpen"
  1264. :footer="null"
  1265. width="1080px"
  1266. :centered="true"
  1267. wrapClassName="select-template-modal"
  1268. :destroyOnClose="true"
  1269. >
  1270. <template #title>
  1271. <div class="cleaning-modal-title">
  1272. <span class="title-main">选择模板</span>
  1273. </div>
  1274. </template>
  1275. <div class="template-modal-layout">
  1276. <aside class="modal-left">
  1277. <div class="modal-panel-head">
  1278. <div>
  1279. <div class="panel-title">模板分类</div>
  1280. </div>
  1281. <span class="head-badge">{{ Math.max(categories.length - 1, 0) }} 类</span>
  1282. </div>
  1283. <Button type="primary" block class="create-template-btn">+创建新模板</Button>
  1284. <div class="cate-list">
  1285. <div
  1286. v-for="cate in categories"
  1287. :key="cate"
  1288. class="cate-item"
  1289. :class="{ active: activeCategory === cate }"
  1290. @click="activeCategory = cate"
  1291. >
  1292. <span class="cate-name">{{ cate }}</span>
  1293. <span class="cate-count">{{ getCategoryCount(cate) }}</span>
  1294. </div>
  1295. </div>
  1296. </aside>
  1297. <section class="modal-mid">
  1298. <div class="modal-panel-head">
  1299. <div>
  1300. <div class="panel-title">模板列表</div>
  1301. </div>
  1302. <div class="modal-panel-head__actions">
  1303. <div class="tab-row" v-show="showTemplateTabSwitch">
  1304. <span
  1305. class="tab"
  1306. :class="{ active: activeTemplateTab === 'template' }"
  1307. @click="activeTemplateTab = 'template'"
  1308. >模板</span
  1309. >
  1310. <span
  1311. class="tab"
  1312. :class="{ active: activeTemplateTab === 'plugin' }"
  1313. @click="activeTemplateTab = 'plugin'"
  1314. >模板插件</span
  1315. >
  1316. </div>
  1317. <span class="head-badge">{{ filteredTemplateList.length }} 个</span>
  1318. </div>
  1319. </div>
  1320. <div class="filters">
  1321. <div class="filter-item filter-item--grow">
  1322. <span class="filter-label">模板名称</span>
  1323. <Input v-model:value="templateKeyword" placeholder="输入模板名称查询" />
  1324. </div>
  1325. <div class="filter-item">
  1326. <span class="filter-label">来源</span>
  1327. <Select v-model:value="templateSource" :options="templateSourceOptions" class="source-select" />
  1328. </div>
  1329. </div>
  1330. <div class="list-summary">
  1331. <span>当前范围:{{ activeTemplateTab === 'plugin' ? '模板插件' : activeCategory }}</span>
  1332. <span>已选:{{ modalTemplateItem ? getTemplateDisplayName(modalTemplateItem) : '未选择' }}</span>
  1333. </div>
  1334. <div class="tpl-list">
  1335. <div
  1336. v-for="tpl in filteredTemplateList"
  1337. :key="String(tpl.id)"
  1338. class="tpl-item"
  1339. :class="{ active: modalTemplateItem?.id === tpl.id }"
  1340. @click="handleTemplateItemClick(tpl)"
  1341. @dblclick="handleTemplateItemConfirm(tpl)"
  1342. >
  1343. <div class="tpl-main">
  1344. <div class="name">{{ getTemplateDisplayName(tpl) }}</div>
  1345. <div class="tpl-meta">
  1346. <span class="tpl-meta-text">{{ tpl.classify || '未分类' }}</span>
  1347. <span class="tpl-meta-text">{{ getTemplateSourceLabel(tpl) }}</span>
  1348. <span class="tpl-meta-text">表头 {{ getTemplateHeaderCount(tpl) }} 项</span>
  1349. </div>
  1350. </div>
  1351. <span v-if="modalTemplateItem?.id === tpl.id" class="selected-mark">已选</span>
  1352. </div>
  1353. <div v-if="!filteredTemplateList.length" class="empty">暂无模板</div>
  1354. </div>
  1355. </section>
  1356. <section class="modal-right">
  1357. <div class="modal-panel-head">
  1358. <div>
  1359. <div class="panel-title">模板规则预览</div>
  1360. </div>
  1361. <span class="head-badge">{{ previewRules.length }} 条</span>
  1362. </div>
  1363. <template v-if="modalPreviewTemplate">
  1364. <div class="preview-overview">
  1365. <div class="overview-card">
  1366. <span class="overview-label">模板名称</span>
  1367. <span class="overview-value">{{ getTemplateDisplayName(modalPreviewTemplate) }}</span>
  1368. </div>
  1369. <div class="overview-card">
  1370. <span class="overview-label">模板来源</span>
  1371. <span class="overview-value">{{ modalTemplateSourceLabel }}</span>
  1372. </div>
  1373. <div class="overview-card">
  1374. <span class="overview-label">分类</span>
  1375. <span class="overview-value">{{ modalPreviewTemplate.classify || '未分类' }}</span>
  1376. </div>
  1377. <div class="overview-card">
  1378. <span class="overview-label">字段数</span>
  1379. <span class="overview-value">{{ modalTemplateFieldCount }}</span>
  1380. </div>
  1381. </div>
  1382. <div class="preview-meta">
  1383. <div class="preview-meta-row preview-meta-row--headers">
  1384. <span class="meta-label">表头预览</span>
  1385. <span class="meta-value meta-value--scroll">
  1386. {{ modalTemplateHeaderList.length ? modalTemplateHeaderList.join(' / ') : '暂无表头信息' }}
  1387. </span>
  1388. </div>
  1389. </div>
  1390. <div class="preview-rule-title">模板规则</div>
  1391. <div class="preview-rule-table">
  1392. <div class="rule-head">
  1393. <span>目标字段</span>
  1394. <span>原始字段</span>
  1395. </div>
  1396. <div class="rule-row" v-for="(row, idx) in previewRules" :key="idx">
  1397. <span class="rule-cell rule-cell--target">
  1398. <span class="rule-target-label" :title="row.targetField">
  1399. <span v-if="row.required" class="rule-required-mark">*</span>
  1400. <span class="rule-target-text">{{ row.targetField }}</span>
  1401. </span>
  1402. <span v-if="row.required" class="rule-required-badge">必填</span>
  1403. </span>
  1404. <span class="rule-cell" :title="row.originalField">{{ row.originalField }}</span>
  1405. </div>
  1406. <div v-if="templatePreviewLoading" class="empty">模板详情加载中...</div>
  1407. <div v-else-if="!previewRules.length" class="empty">暂无规则</div>
  1408. </div>
  1409. </template>
  1410. <template v-else>
  1411. <div class="preview-empty">
  1412. <div class="empty-title">请选择模板</div>
  1413. <div class="empty-desc">左侧选择分类,中间点击具体模板后,这里会展示字段规则与基础信息。</div>
  1414. </div>
  1415. </template>
  1416. <div class="modal-actions">
  1417. <Button type="primary" :disabled="!modalTemplateItem || isPluginTemplateTab" @click="confirmTemplateSelect">
  1418. 确认选择
  1419. </Button>
  1420. <Button @click="closeTemplateModal">取消</Button>
  1421. </div>
  1422. </section>
  1423. </div>
  1424. </Modal>
  1425. </div>
  1426. </template>
  1427. <script lang="ts" setup name="ViewsDataCleaning">
  1428. import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
  1429. import { DownOutlined, UpOutlined } from '@ant-design/icons-vue';
  1430. import { Button, Checkbox, Input, InputNumber, Modal, Popover, Radio, Select, Switch, Tooltip } from 'ant-design-vue';
  1431. import { EmptyState } from '@/core/components/EmptyState';
  1432. import { BasicTree } from '@/core/components/Tree';
  1433. import { useDesign } from '@/core/hooks/web/useDesign';
  1434. import { useGo, useQuery } from '@/core/hooks/web/usePage';
  1435. import { PageEnum } from '@/core/enums/pageEnum';
  1436. import { useMessage } from '@/core/hooks/web/useMessage';
  1437. import type { FileInfo } from '@/types/api';
  1438. import { tplCategoryList, tplCheckTableName, tplDetail, tplPage } from '@/plat/api/tpl/tplApi';
  1439. import { rawPreview } from '@/case/api/govern/governApi';
  1440. import type { TableInfo } from '@/plat/api/tpl/types/dto';
  1441. import {
  1442. FunAbs,
  1443. FunDate,
  1444. FunDateTime,
  1445. FunExtra,
  1446. FunMultiplier,
  1447. FunRadix,
  1448. FunRegular,
  1449. FunReplace,
  1450. FunSpExtra,
  1451. FunTime,
  1452. MappingEnum,
  1453. RuleMapping,
  1454. RuleSplit,
  1455. } from '../../../dm/types/Func';
  1456. import {
  1457. buildRemoveSpecialRegex,
  1458. groupColumnsFnValue,
  1459. normalizeDecimalBaseLabel,
  1460. runCleaningPreviewPipeline,
  1461. type CleaningRuleKey as RulePipelineKey,
  1462. } from './cleaningRuleUtils';
  1463. import { resetImportProgressSession } from './importProgressSession';
  1464. import { FileStatusEnum } from '../../types/enum';
  1465. type TreeTableRuleInfo = {
  1466. id?: string | number | null;
  1467. tableId?: string | number | null;
  1468. fieldNameCn?: string | null;
  1469. fieldNameEn?: string | null;
  1470. fieldType?: string | null;
  1471. required?: number | null;
  1472. fieldLen?: number | null;
  1473. matched?: number | null;
  1474. matchedRequired?: number | null;
  1475. directionConf?: string | null;
  1476. zf?: number | string | boolean | null;
  1477. fieldSort?: number | null;
  1478. fileColIndex?: number | null;
  1479. fileColName?: string | null;
  1480. funList?: TreeStoredFunRule[] | null;
  1481. columnRule?: TreeStoredColumnRule | null;
  1482. };
  1483. type TreeStoredFunRule = {
  1484. '@class'?: string;
  1485. key?: string;
  1486. ruleKey?: string;
  1487. name?: string;
  1488. sort?: number | null;
  1489. rules?: Record<string, any>;
  1490. [key: string]: any;
  1491. };
  1492. type TreeStoredColumnRule = {
  1493. '@class'?: string;
  1494. mode?: 'ref' | 'custom' | 'split';
  1495. contentFields?: string[];
  1496. customContent?: string;
  1497. source?: string;
  1498. delimiterType?: string;
  1499. delimiterValue?: string;
  1500. targetName1?: string;
  1501. targetName2?: string;
  1502. refIndex?: number | number[] | null;
  1503. mappingEnum?: number | null;
  1504. splitNameIndex?: number | null;
  1505. splitNameIndex2?: number | null;
  1506. content?: string | null;
  1507. len?: number | null;
  1508. splitChar?: string | null;
  1509. [key: string]: any;
  1510. };
  1511. type TreeFileInfo = Omit<
  1512. FileInfo,
  1513. | 'id'
  1514. | 'pid'
  1515. | 'children'
  1516. | 'tableName'
  1517. | 'templateId'
  1518. | 'fileType'
  1519. | 'dataNum'
  1520. | 'fileSize'
  1521. | 'fileStatus'
  1522. | 'lineNo'
  1523. | 'batchId'
  1524. | 'successNum'
  1525. | 'failNum'
  1526. | 'charSet'
  1527. | 'delimiter'
  1528. | 'header'
  1529. | 'tableRuleList'
  1530. > & {
  1531. id?: string | number | null;
  1532. pid?: string | number | null;
  1533. tableName?: string | null;
  1534. templateId?: string | number | null;
  1535. fileType?: string | null;
  1536. dataNum?: string | number | null;
  1537. fileSize?: string | number | null;
  1538. fileStatus?: string | null;
  1539. lineNo?: number | null;
  1540. batchId?: string | number | null;
  1541. successNum?: string | number | null;
  1542. failNum?: string | number | null;
  1543. charSet?: string | null;
  1544. delimiter?: string | null;
  1545. header?: string | string[] | null;
  1546. tableRuleList?: TreeTableRuleInfo[] | null;
  1547. ignoreLines?: number[] | null;
  1548. groupColumns?: Array<{ targetHeader: string; groupFlag: boolean; groupColumns: Array<Record<string, any>> }> | null;
  1549. funcRegx?: string | null;
  1550. childTemplateName?: string | null;
  1551. children?: TreeFileInfo[];
  1552. };
  1553. type PreviewFileDataset = {
  1554. headers: string[];
  1555. rows: Record<string, string>[];
  1556. };
  1557. type TemplateListItem = TableInfo & { source?: number; hasInner?: string | number | boolean | null };
  1558. type MappingTargetField = { key: string; label: string; required: boolean };
  1559. type FormatValidationType =
  1560. | 'number'
  1561. | 'date'
  1562. | 'dateTime'
  1563. | 'time'
  1564. | 'loanFlag'
  1565. | 'callType'
  1566. | 'phone'
  1567. | 'acctCardNo'
  1568. | 'idCardNumber';
  1569. type RecognitionDirectionComparableMap = Record<RecognitionRuleTabKey, Set<string>>;
  1570. type HeaderFormatValidationState = {
  1571. invalidTargets: Array<{
  1572. key: string;
  1573. label: string;
  1574. formatType: FormatValidationType;
  1575. invalidValue: string;
  1576. }>;
  1577. title: string;
  1578. };
  1579. type RecognitionBasicInfoRow = {
  1580. key: string;
  1581. headerMatch: string;
  1582. targetField: string;
  1583. endFlag: string;
  1584. };
  1585. type RecognitionDirectionItem = {
  1586. key: string;
  1587. label: string;
  1588. value: string;
  1589. };
  1590. type RecognitionRuleTabKey = '借贷标志' | '通信类别';
  1591. type RecognitionDirectionField = {
  1592. key: string;
  1593. label: string;
  1594. tab: RecognitionRuleTabKey;
  1595. entries: RecognitionDirectionItem[];
  1596. };
  1597. const { prefixCls } = useDesign('case-data-cleaning');
  1598. const go = useGo();
  1599. const pageQuery = useQuery();
  1600. const { createMessage } = useMessage();
  1601. const importingData = ref(false);
  1602. const leftCollapsed = ref(false);
  1603. const leftPanelWidth = ref(300);
  1604. const middlePanelWidth = ref(520);
  1605. const minLeftWidth = 220;
  1606. const maxLeftWidth = 520;
  1607. const minMiddleWidth = 360;
  1608. const maxMiddleWidth = 900;
  1609. const minRightWidth = 560;
  1610. const resizeState = ref<{ type: 'left' | 'middle' | ''; startX: number; startLeft: number; startMiddle: number }>({
  1611. type: '',
  1612. startX: 0,
  1613. startLeft: 300,
  1614. startMiddle: 520,
  1615. });
  1616. const pendingClientX = ref<number | null>(null);
  1617. const rafId = ref<number | null>(null);
  1618. const layoutTemplateColumns = computed(() => {
  1619. if (leftCollapsed.value) {
  1620. return `${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
  1621. }
  1622. return `${leftPanelWidth.value}px 6px ${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
  1623. });
  1624. const sortMode = ref<'asc' | 'desc' | ''>('');
  1625. const selectedKeys = ref<string[]>([]);
  1626. interface FileTreeNode {
  1627. key: string;
  1628. title: string;
  1629. children?: FileTreeNode[];
  1630. raw?: TreeFileInfo;
  1631. selectable?: boolean;
  1632. }
  1633. type SheetNodeVisualState = 'success' | 'fail' | 'analyzing';
  1634. const fileStatusLabelMap = Object.entries(FileStatusEnum).reduce(
  1635. (acc, [key, label]) => {
  1636. acc[key] = label;
  1637. return acc;
  1638. },
  1639. {} as Record<string, string>,
  1640. );
  1641. const normalizeNodeId = (value?: string | number | null, fallback = '') => {
  1642. const normalized = String(value ?? '').trim();
  1643. return normalized || fallback;
  1644. };
  1645. const normalizeParentId = (value?: string | number | null) => {
  1646. const normalized = String(value ?? '').trim();
  1647. return !normalized || normalized === '0' ? '' : normalized;
  1648. };
  1649. const isFileRootItem = (item: TreeFileInfo) => !normalizeParentId(item.pid);
  1650. const hasChildItems = (item: TreeFileInfo) => Array.isArray(item.children) && item.children.length > 0;
  1651. const getFallbackTreeKey = (item: TreeFileInfo) =>
  1652. [
  1653. item.filePath || item.fileName || 'file',
  1654. item.sheetName || 'root',
  1655. item.tableName || '',
  1656. item.lineNo ?? '',
  1657. item.createDateTime || '',
  1658. ].join('__');
  1659. const getTreeNodeKey = (item: TreeFileInfo) => normalizeNodeId(item.id, getFallbackTreeKey(item));
  1660. const normalizeTemplateId = (value?: string | number | null) => {
  1661. const next = Number(value);
  1662. return Number.isFinite(next) && next > 0 ? next : 0;
  1663. };
  1664. const getRequestErrorMessage = (error: any, fallback = '操作失败') =>
  1665. error?.response?.data?.message || error?.message || fallback;
  1666. const normalizeTemplateHeaders = (headers?: string | string[] | null) => {
  1667. if (Array.isArray(headers)) {
  1668. return headers.map((item) => String(item ?? '').trim()).filter(Boolean);
  1669. }
  1670. return String(headers || '')
  1671. .split(/[,,]/)
  1672. .map((item) => item.trim())
  1673. .filter(Boolean);
  1674. };
  1675. const createTemplateFieldKey = (value: string | undefined, index: number) => {
  1676. const normalized = String(value || '').trim();
  1677. return normalized ? `field_${index + 1}_${normalized}` : `field_${index + 1}`;
  1678. };
  1679. const normalizeRuleText = (value?: unknown) => String(value ?? '').trim();
  1680. const normalizeTemplateClassifyText = (value?: unknown) => normalizeRuleText(value);
  1681. const normalizeTemplateNameKey = (value?: unknown) => normalizeRuleText(value).toLocaleLowerCase();
  1682. const normalizeRecognitionConfigText = (value?: unknown) => String(value ?? '').trim();
  1683. const normalizeSingleLineText = (value?: unknown) =>
  1684. String(value ?? '')
  1685. .replace(/[\t\r\n\s\u3000]+/g, '')
  1686. .trim();
  1687. const resolveTemplateNameCheckResult = (payload: unknown): boolean | null => {
  1688. if (typeof payload === 'boolean') return payload;
  1689. if (typeof payload === 'number') {
  1690. if (payload === 1) return true;
  1691. if (payload === 0) return false;
  1692. return null;
  1693. }
  1694. if (typeof payload === 'string') {
  1695. const normalized = payload.trim().toLowerCase();
  1696. if (['true', '1', 'yes', 'y', 'ok', 'success'].includes(normalized)) return true;
  1697. if (['false', '0', 'no', 'n', 'fail'].includes(normalized)) return false;
  1698. return null;
  1699. }
  1700. if (!payload || typeof payload !== 'object') return null;
  1701. const record = payload as Record<string, unknown>;
  1702. for (const key of ['valid', 'available', 'pass', 'result', 'data']) {
  1703. const resolved = resolveTemplateNameCheckResult(record[key]);
  1704. if (resolved !== null) return resolved;
  1705. }
  1706. const existsResult = resolveTemplateNameCheckResult(record.exists);
  1707. if (existsResult !== null) return !existsResult;
  1708. return null;
  1709. };
  1710. const createRecognitionBasicInfoKey = () => `merge_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  1711. const normalizeFieldMatchText = (value?: string | number | null) =>
  1712. normalizeRuleText(value)
  1713. .replace(/[\s_\-—()()[\]【】{}<>《》"'`‘’“”、,,.。::;;/\\|]/g, '')
  1714. .toLowerCase();
  1715. const normalizeTableRuleList = (list?: TreeTableRuleInfo[] | null) =>
  1716. Array.isArray(list)
  1717. ? list.filter(
  1718. (item) =>
  1719. item && [item.fieldNameCn, item.fieldNameEn, item.fileColName].some((val) => normalizeRuleText(val)),
  1720. )
  1721. : [];
  1722. const getIntegerRuleFlag = (value: unknown) => {
  1723. const normalizedValue = Number(value);
  1724. return Number.isInteger(normalizedValue) ? normalizedValue : null;
  1725. };
  1726. const getRawRuleFlagValue = (
  1727. source: unknown,
  1728. key: 'required' | 'matched' | 'matchedRequired' | 'zf',
  1729. ): number | string | null | undefined => {
  1730. const record = (source || {}) as Record<string, any>;
  1731. if (!Object.prototype.hasOwnProperty.call(record, key)) return undefined;
  1732. const value = record[key];
  1733. if (value === undefined) return undefined;
  1734. if (value === null) return null;
  1735. const normalizedValue = Number(value);
  1736. return Number.isInteger(normalizedValue) ? normalizedValue : value;
  1737. };
  1738. const pickFirstDefinedRuleFlagValue = (...values: Array<number | string | null | undefined>) => {
  1739. for (const value of values) {
  1740. if (value !== undefined) {
  1741. return value;
  1742. }
  1743. }
  1744. return undefined;
  1745. };
  1746. /**
  1747. * 解析规则里的「正负转换」开关(zf)。
  1748. *
  1749. * <p>后端 TableRuleDTO.zf 是包装类型 Integer,清洗器以 `rule.getZf() == 1` 判断(会自动拆箱),
  1750. * 一旦这里输出 undefined,JSON 序列化会丢掉整个键 → 后端拿到 null → 清洗任务直接抛 NPE。
  1751. * 因此本函数保证**始终返回整数**:优先模板字段的 zf,其次上一条规则的 zf,兜底 0。
  1752. */
  1753. const resolveStoredZfFlag = (templateField?: unknown, previous?: TreeTableRuleInfo | null) =>
  1754. getIntegerRuleFlag(
  1755. pickFirstDefinedRuleFlagValue(getRawRuleFlagValue(templateField, 'zf'), getRawRuleFlagValue(previous, 'zf'), 0),
  1756. ) ?? 0;
  1757. const getTableRuleRequiredFlag = (item?: TreeTableRuleInfo | null, fallback = false) => {
  1758. if (!item) return fallback;
  1759. const required = getIntegerRuleFlag(item.required);
  1760. if (required !== null) {
  1761. return required === 1;
  1762. }
  1763. return fallback;
  1764. };
  1765. const getTableRuleMatchedDisplayFlag = (item?: TreeTableRuleInfo | null, fallback = false) => {
  1766. if (!item) return fallback;
  1767. const matched = getIntegerRuleFlag(item.matched);
  1768. if (matched !== null) {
  1769. return matched === 1;
  1770. }
  1771. const required = getIntegerRuleFlag(item.required);
  1772. if (required !== null) {
  1773. return required === 1;
  1774. }
  1775. return fallback;
  1776. };
  1777. const getTableRuleTargetLabel = (item: TreeTableRuleInfo, index: number) =>
  1778. normalizeRuleText(item.fieldNameCn) || normalizeRuleText(item.fieldNameEn) || `字段${index + 1}`;
  1779. const getTableRuleTargetKey = (item: TreeTableRuleInfo, index: number) =>
  1780. normalizeRuleText(item.fieldNameEn) || createTemplateFieldKey(getTableRuleTargetLabel(item, index), index);
  1781. const buildTargetFieldsFromRuleList = (list: TreeTableRuleInfo[]): MappingTargetField[] => {
  1782. const usedKeys = new Set<string>();
  1783. return normalizeTableRuleList(list).reduce((acc, item, index) => {
  1784. const key = getTableRuleTargetKey(item, index);
  1785. if (!key || usedKeys.has(key)) return acc;
  1786. usedKeys.add(key);
  1787. acc.push({
  1788. key,
  1789. label: getTableRuleTargetLabel(item, index),
  1790. required: getTableRuleRequiredFlag(item),
  1791. });
  1792. return acc;
  1793. }, [] as MappingTargetField[]);
  1794. };
  1795. const getTemplateFieldRequired = (value: unknown) => {
  1796. if (value === null || value === undefined || value === '') return false;
  1797. if (typeof value === 'boolean') return value;
  1798. const parsed = Number(value);
  1799. return !Number.isNaN(parsed) && parsed === 1;
  1800. };
  1801. const getTemplateFieldRecord = (item: unknown) => (item || {}) as Record<string, any>;
  1802. const getTemplateFieldRequiredFlag = (item: unknown, fallback = false) => {
  1803. const field = getTemplateFieldRecord(item);
  1804. if (Object.prototype.hasOwnProperty.call(field, 'required')) {
  1805. return getTemplateFieldRequired(field.required);
  1806. }
  1807. return fallback;
  1808. };
  1809. const getTemplateFieldMatchedDisplayFlag = (item: unknown, fallback = false) => {
  1810. const field = getTemplateFieldRecord(item);
  1811. if (Object.prototype.hasOwnProperty.call(field, 'matched')) {
  1812. return getTemplateFieldRequired(field.matched);
  1813. }
  1814. if (Object.prototype.hasOwnProperty.call(field, 'required')) {
  1815. return getTemplateFieldRequired(field.required);
  1816. }
  1817. return fallback;
  1818. };
  1819. const getOrderedTemplateFields = (fields?: TableInfo['tableFields'] | null) =>
  1820. (Array.isArray(fields) ? fields : [])
  1821. .filter((item) => {
  1822. const field = getTemplateFieldRecord(item);
  1823. return !!(field.fieldComment || field.fieldNameCn || field.fieldNameEn || field.fieldName);
  1824. })
  1825. .map((item, index) => {
  1826. const field = getTemplateFieldRecord(item);
  1827. const sort = Number(field.fieldSort);
  1828. return {
  1829. item,
  1830. index,
  1831. required: getTemplateFieldRequiredFlag(item),
  1832. sort: Number.isFinite(sort) && sort > 0 ? sort : index + 1,
  1833. };
  1834. })
  1835. .sort((a, b) => {
  1836. if (a.required !== b.required) {
  1837. return a.required ? -1 : 1;
  1838. }
  1839. return a.sort === b.sort ? a.index - b.index : a.sort - b.sort;
  1840. })
  1841. .map(({ item }) => item);
  1842. const getTemplateFieldTargetLabel = (item: unknown, index: number) => {
  1843. const field = getTemplateFieldRecord(item);
  1844. return String(field.fieldComment || field.fieldNameCn || field.fieldName || `字段${index + 1}`);
  1845. };
  1846. const getTemplateFieldTargetKey = (item: unknown, index: number) => {
  1847. const field = getTemplateFieldRecord(item);
  1848. return String(
  1849. field.fieldNameEn ||
  1850. field.fieldName ||
  1851. createTemplateFieldKey(String(field.fieldComment || field.fieldNameCn || ''), index),
  1852. );
  1853. };
  1854. const isTransAmountTargetField = (field: Pick<MappingTargetField, 'key' | 'label'>) =>
  1855. normalizeRuleText(field.key) === 'transAmount' || normalizeFieldMatchText(field.label).includes('交易金额');
  1856. const isLoanFlagTargetField = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
  1857. const targetKey = normalizeRuleText(field.key);
  1858. const targetLabel = normalizeRuleText(field.label);
  1859. return targetKey === 'debitCreditFlag' || targetKey === 'loanFlag' || targetLabel.includes('借贷');
  1860. };
  1861. const getZfFlag = (value: unknown) => {
  1862. if (typeof value === 'boolean') return value;
  1863. const normalizedValue = normalizeRuleText(value as string | number | null).toLowerCase();
  1864. return ['1', 'true', 'yes', '是'].includes(normalizedValue);
  1865. };
  1866. const getRecordZfValue = (source: unknown) => {
  1867. const record = (source || {}) as Record<string, any>;
  1868. if (!Object.prototype.hasOwnProperty.call(record, 'zf')) return undefined;
  1869. return record.zf === null || record.zf === undefined || record.zf === '' ? undefined : record.zf;
  1870. };
  1871. const getTableRuleTargetField = (item: TreeTableRuleInfo, index: number) => ({
  1872. key: getTableRuleTargetKey(item, index),
  1873. label: getTableRuleTargetLabel(item, index),
  1874. });
  1875. const findTransAmountTableRule = (list?: TreeTableRuleInfo[] | null) =>
  1876. normalizeTableRuleList(list).find((item, index) =>
  1877. isTransAmountTargetField(getTableRuleTargetField(item, index)),
  1878. ) || null;
  1879. const findTransAmountTemplateFieldMeta = (template?: Partial<TableInfo> | null) =>
  1880. getOrderedTemplateFields(template?.tableFields).find((item, index) =>
  1881. isTransAmountTargetField({
  1882. key: getTemplateFieldTargetKey(item, index),
  1883. label: getTemplateFieldTargetLabel(item, index),
  1884. }),
  1885. ) || null;
  1886. const buildTargetFieldsFromTemplateInfo = (template?: Partial<TableInfo> | null): MappingTargetField[] => {
  1887. const tableFields = getOrderedTemplateFields(template?.tableFields);
  1888. if (tableFields.length) {
  1889. return tableFields.map((item, index) => ({
  1890. key: getTemplateFieldTargetKey(item, index),
  1891. label: getTemplateFieldTargetLabel(item, index),
  1892. required: getTemplateFieldRequiredFlag(item),
  1893. }));
  1894. }
  1895. return [];
  1896. };
  1897. const inferTemplateClassify = (item: Pick<TreeFileInfo, 'fileName' | 'tableName' | 'sheetName'>) => {
  1898. const text = `${item.fileName || ''}${item.tableName || ''}${item.sheetName || ''}`;
  1899. if (text.includes('通信')) return '通信类';
  1900. if (text.includes('银行') || text.includes('账户') || text.includes('交易')) return '金融类';
  1901. if (text.includes('民航') || text.includes('铁路') || text.includes('旅馆') || text.includes('快递'))
  1902. return '轨迹类';
  1903. if (text.includes('公安') || text.includes('户籍') || text.includes('违法') || text.includes('护照'))
  1904. return '公安类';
  1905. return '未分类';
  1906. };
  1907. const FILE_INFO_STORAGE_KEY = 'case_clean_file_infos';
  1908. const IMPORT_FILE_INFO_STORAGE_KEY = 'case_clean_import_file_infos';
  1909. const mockFileList: TreeFileInfo[] = [];
  1910. const buildTreeFileInfoIdMap = (list: TreeFileInfo[]) => {
  1911. const infoMap = new Map<string, TreeFileInfo>();
  1912. const walk = (nodes?: TreeFileInfo[] | null) => {
  1913. if (!Array.isArray(nodes)) return;
  1914. nodes.forEach((node) => {
  1915. const nodeId = normalizeRuleText(node?.id);
  1916. if (nodeId) {
  1917. infoMap.set(nodeId, node);
  1918. }
  1919. if (Array.isArray(node?.children) && node.children.length) {
  1920. walk(node.children);
  1921. }
  1922. });
  1923. };
  1924. walk(list);
  1925. return infoMap;
  1926. };
  1927. const mockFileInfoById = buildTreeFileInfoIdMap(mockFileList);
  1928. const mergeStoredTableRuleListWithFallback = (
  1929. storedList?: TreeTableRuleInfo[] | null,
  1930. fallbackList?: TreeTableRuleInfo[] | null,
  1931. ) => {
  1932. const storedRules = normalizeTableRuleList(storedList);
  1933. const fallbackRules = normalizeTableRuleList(fallbackList);
  1934. if (!storedRules.length) {
  1935. return fallbackRules.map((item) => sanitizeTreeTableRuleInfo(item));
  1936. }
  1937. if (!fallbackRules.length) {
  1938. return storedRules.map((item) => sanitizeTreeTableRuleInfo(item));
  1939. }
  1940. const fallbackRuleMap = fallbackRules.reduce(
  1941. (acc, item, index) => {
  1942. const key = getTableRuleTargetKey(item, index);
  1943. const label = normalizeFieldMatchText(getTableRuleTargetLabel(item, index));
  1944. if (key) acc[key] = item;
  1945. if (label) acc[label] = item;
  1946. return acc;
  1947. },
  1948. {} as Record<string, TreeTableRuleInfo>,
  1949. );
  1950. return storedRules.map((item, index) => {
  1951. const key = getTableRuleTargetKey(item, index);
  1952. const label = normalizeFieldMatchText(getTableRuleTargetLabel(item, index));
  1953. const fallbackRule = (key && fallbackRuleMap[key]) || (label && fallbackRuleMap[label]) || null;
  1954. return sanitizeTreeTableRuleInfo({
  1955. ...cloneDeepSafe(fallbackRule || {}),
  1956. ...cloneDeepSafe(item),
  1957. } as TreeTableRuleInfo);
  1958. });
  1959. };
  1960. const mergeStoredFileInfoWithMock = (item: TreeFileInfo): TreeFileInfo => {
  1961. const mockItem = mockFileInfoById.get(normalizeRuleText(item?.id));
  1962. const mergedChildrenSource = Array.isArray(item.children) ? item.children : mockItem?.children;
  1963. return sanitizeTreeFileInfo({
  1964. ...cloneDeepSafe(mockItem || item),
  1965. ...cloneDeepSafe(item),
  1966. filePath: normalizeRuleText(mockItem?.filePath) || item.filePath,
  1967. tableRuleList: mergeStoredTableRuleListWithFallback(item.tableRuleList, mockItem?.tableRuleList),
  1968. children: Array.isArray(mergedChildrenSource)
  1969. ? mergedChildrenSource.map((child) => mergeStoredFileInfoWithMock(child))
  1970. : [],
  1971. });
  1972. };
  1973. const buildMockTemplateList = (list: TreeFileInfo[]): TemplateListItem[] => {
  1974. const templateMap = new Map<number, TemplateListItem>();
  1975. list.forEach((item) => {
  1976. const templateId = normalizeTemplateId(item.templateId);
  1977. if (!templateId || templateMap.has(templateId)) return;
  1978. const tableRules = normalizeTableRuleList(item.tableRuleList);
  1979. const headers = normalizeTemplateHeaders(item.header);
  1980. templateMap.set(templateId, {
  1981. id: templateId,
  1982. tableNameCn: item.tableName || item.sheetName || `模板${templateId}`,
  1983. tableNameEn: `template_${templateId}`,
  1984. classify: inferTemplateClassify(item),
  1985. headers: headers.join(','),
  1986. funcRegx: normalizeRecognitionConfigText(item.funcRegx) || undefined,
  1987. tableFields: tableRules.length
  1988. ? tableRules.map((rule, index) => ({
  1989. fieldName: getTableRuleTargetKey(rule, index),
  1990. fieldType: rule.fieldType || 'string',
  1991. fieldComment: getTableRuleTargetLabel(rule, index),
  1992. required: getTableRuleRequiredFlag(rule) ? 1 : 0,
  1993. matched: getTableRuleMatchedDisplayFlag(rule) ? 1 : 0,
  1994. }))
  1995. : headers.map((header, index) => ({
  1996. fieldName: createTemplateFieldKey(header, index),
  1997. fieldType: 'string',
  1998. fieldComment: header,
  1999. })),
  2000. source: 1,
  2001. });
  2002. });
  2003. return Array.from(templateMap.values());
  2004. };
  2005. const fileList = ref<TreeFileInfo[]>([...mockFileList]);
  2006. const mockTemplateList = buildMockTemplateList(mockFileList);
  2007. const mergeTemplateCollections = (...collections: TemplateListItem[][]) => {
  2008. const templateMap = new Map<string, TemplateListItem>();
  2009. collections.forEach((collection) => {
  2010. collection.forEach((item, index) => {
  2011. const templateId = normalizeTemplateId(item?.id);
  2012. const key = templateId ? String(templateId) : `${item?.tableNameCn || 'template'}_${index}`;
  2013. templateMap.set(key, item);
  2014. });
  2015. });
  2016. return Array.from(templateMap.values());
  2017. };
  2018. const getFileStatusLabel = (status?: string | null) => {
  2019. const normalized = String(status || '').trim();
  2020. if (!normalized) return '待处理';
  2021. return fileStatusLabelMap[normalized] || normalized;
  2022. };
  2023. const getFileStatusVisualState = (status?: string | null) => {
  2024. const normalized = String(status || '').trim();
  2025. if (
  2026. normalized === 'SHEET_TEMPLATE_MATCHED_SUCCESS' ||
  2027. normalized === FileStatusEnum.SHEET_TEMPLATE_MATCHED_SUCCESS
  2028. ) {
  2029. return 'success' as const;
  2030. }
  2031. if (normalized === 'SHEET_TEMPLATE_MATCHED_FAIL' || normalized === FileStatusEnum.SHEET_TEMPLATE_MATCHED_FAIL) {
  2032. return 'fail' as const;
  2033. }
  2034. return null;
  2035. };
  2036. const isTemplateMatchedFailStatus = (status?: string | null) =>
  2037. ['SHEET_TEMPLATE_MATCHED_FAIL', FileStatusEnum.SHEET_TEMPLATE_MATCHED_FAIL].includes(String(status || '').trim());
  2038. const isTreeFileNode = (item?: TreeFileInfo | null): item is TreeFileInfo => !!item && isFileRootItem(item);
  2039. const isTreeSheetNode = (item?: TreeFileInfo | null): item is TreeFileInfo => !!item && !isFileRootItem(item);
  2040. const isTableRuleMatched = (
  2041. item?: TreeTableRuleInfo | null,
  2042. options: { index?: number; amountZfEnabled?: boolean } = {},
  2043. ) => {
  2044. if (!item) return false;
  2045. if (
  2046. options.amountZfEnabled &&
  2047. options.index !== undefined &&
  2048. isLoanFlagTargetField(getTableRuleTargetField(item, options.index))
  2049. ) {
  2050. return true;
  2051. }
  2052. const fileColIndex = Number(item.fileColIndex);
  2053. return (Number.isInteger(fileColIndex) && fileColIndex >= 0) || !!normalizeRuleText(item.fileColName);
  2054. };
  2055. const isMatchSuccessful = (requiredTotal: number, requiredMatched: number, matchedCount: number) => {
  2056. if (requiredTotal > 0) {
  2057. return requiredMatched === requiredTotal;
  2058. }
  2059. return matchedCount > 0;
  2060. };
  2061. const getTableRuleMatchStats = (list?: TreeTableRuleInfo[] | null) => {
  2062. const normalizedRules = normalizeTableRuleList(list);
  2063. const amountZfEnabled = normalizedRules.some(
  2064. (item, index) => isTransAmountTargetField(getTableRuleTargetField(item, index)) && getZfFlag(item.zf),
  2065. );
  2066. const normalizedRuleItems = normalizedRules.map((item, index) => ({ item, index }));
  2067. const requiredRules = normalizedRuleItems.filter(({ item }) => getTableRuleRequiredFlag(item));
  2068. const requiredTotal = requiredRules.length;
  2069. const requiredMatched = requiredRules.filter(({ item, index }) =>
  2070. isTableRuleMatched(item, { index, amountZfEnabled }),
  2071. ).length;
  2072. const matchedCount = normalizedRuleItems.filter(({ item, index }) =>
  2073. isTableRuleMatched(item, { index, amountZfEnabled }),
  2074. ).length;
  2075. return { requiredTotal, requiredMatched, matchedCount };
  2076. };
  2077. const isSheetMatchComplete = (item?: TreeFileInfo | null) => {
  2078. if (!isTreeSheetNode(item)) return false;
  2079. const { requiredTotal, requiredMatched, matchedCount } = getTableRuleMatchStats(item?.tableRuleList);
  2080. return isMatchSuccessful(requiredTotal, requiredMatched, matchedCount);
  2081. };
  2082. const getTreeFileName = (item?: TreeFileInfo | null) => item?.fileName || item?.filePath || '未命名文件';
  2083. const getTreeSheetName = (item?: TreeFileInfo | null) => item?.sheetName || item?.fileName || '未命名工作表';
  2084. const getTreeSheetTitle = (item?: TreeFileInfo | null) =>
  2085. [getTreeSheetName(item), item?.tableName, getFileStatusLabel(item?.fileStatus)].filter(Boolean).join(' / ');
  2086. const getFileNodeTitle = (item: TreeFileInfo) => {
  2087. return isFileRootItem(item) ? getTreeFileName(item) : getTreeSheetName(item);
  2088. };
  2089. const toTreeNode = (item: TreeFileInfo): FileTreeNode => ({
  2090. key: getTreeNodeKey(item),
  2091. title: getFileNodeTitle(item),
  2092. raw: item,
  2093. children: [],
  2094. selectable: !(isFileRootItem(item) && hasChildItems(item)),
  2095. });
  2096. const toTreeNodeDeep = (item: TreeFileInfo): FileTreeNode => {
  2097. const children = (item.children || []).map((child) => toTreeNodeDeep(child));
  2098. const node: FileTreeNode = {
  2099. key: getTreeNodeKey(item),
  2100. title: getFileNodeTitle(item),
  2101. raw: item,
  2102. children,
  2103. selectable: !(isFileRootItem(item) && children.length > 0),
  2104. };
  2105. return node;
  2106. };
  2107. const buildTreeFromFlatList = (list: TreeFileInfo[]) => {
  2108. const keyedList = list.map((item) => ({
  2109. item,
  2110. key: getTreeNodeKey(item),
  2111. }));
  2112. const nodeMap = new Map<string, FileTreeNode>();
  2113. keyedList.forEach(({ item, key }) => {
  2114. nodeMap.set(key, toTreeNode(item));
  2115. });
  2116. const roots: FileTreeNode[] = [];
  2117. keyedList.forEach(({ item, key }) => {
  2118. const node = nodeMap.get(key);
  2119. if (!node) return;
  2120. const parentKey = normalizeParentId(item.pid);
  2121. if (!parentKey) {
  2122. roots.push(node);
  2123. return;
  2124. }
  2125. const parent = nodeMap.get(parentKey);
  2126. if (!parent) {
  2127. roots.push(node);
  2128. return;
  2129. }
  2130. parent.children = parent.children || [];
  2131. parent.children.push(node);
  2132. parent.selectable = false;
  2133. parent.title = getFileNodeTitle({
  2134. ...parent.raw,
  2135. children: parent.children.map((child) => child.raw!).filter(Boolean),
  2136. } as TreeFileInfo);
  2137. });
  2138. return roots;
  2139. };
  2140. const sourceTree = computed<FileTreeNode[]>(() => {
  2141. const list = fileList.value || [];
  2142. if (!list.length) return [];
  2143. // 若后端已返回树结构,则直接按 children 渲染
  2144. const hasNestedChildren = list.some((item) => Array.isArray(item.children) && item.children.length > 0);
  2145. if (hasNestedChildren) {
  2146. return list.map((item) => toTreeNodeDeep(item));
  2147. }
  2148. // 否则按字符串 id/pid 组装树,兼容后端返回的大整数字符串
  2149. return buildTreeFromFlatList(list);
  2150. });
  2151. const sortTreeNodes = (nodes: FileTreeNode[]) => {
  2152. return [...nodes]
  2153. .sort((a, b) => (sortMode.value === 'asc' ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title)))
  2154. .map((node) => ({
  2155. ...node,
  2156. children: node.children?.length ? sortTreeNodes(node.children) : node.children,
  2157. }));
  2158. };
  2159. const treeData = computed(() => {
  2160. const list = sourceTree.value;
  2161. if (!sortMode.value) return list;
  2162. return sortTreeNodes(list);
  2163. });
  2164. const findTreeNodeByKey = (nodes: FileTreeNode[], key: string): FileTreeNode | null => {
  2165. for (const node of nodes) {
  2166. if (node.key === key) return node;
  2167. if (node.children?.length) {
  2168. const matched = findTreeNodeByKey(node.children, key);
  2169. if (matched) return matched;
  2170. }
  2171. }
  2172. return null;
  2173. };
  2174. const findFirstSelectableKey = (nodes: FileTreeNode[]): string => {
  2175. for (const node of nodes) {
  2176. if (node.selectable !== false) {
  2177. return node.key;
  2178. }
  2179. if (node.children?.length) {
  2180. const childKey = findFirstSelectableKey(node.children);
  2181. if (childKey) return childKey;
  2182. }
  2183. }
  2184. return '';
  2185. };
  2186. const resolveSelectableTreeKey = (key: string) => {
  2187. const selectedNode = findTreeNodeByKey(treeData.value, key);
  2188. if (!selectedNode) return '';
  2189. if (selectedNode.selectable !== false) return selectedNode.key;
  2190. return findFirstSelectableKey(selectedNode.children || []);
  2191. };
  2192. const iconMap = {
  2193. unmatched: { key: 'unmatched', label: '未匹配(必选)', color: '#cc3333', icon: '●' },
  2194. matched: { key: 'matched', label: '已匹配', color: '#4ba64b', icon: '●' },
  2195. formatError: { key: 'formatError', label: '格式错误', color: '#f2a100', icon: '!' },
  2196. } as const;
  2197. const iconTips = [iconMap.unmatched, iconMap.matched, iconMap.formatError];
  2198. const activeNodeKey = ref<string>('');
  2199. const selectedTemplateId = ref<number>(0);
  2200. const templateDetail = ref<TableInfo | null>(null);
  2201. const templateDetailFromMock = ref(false);
  2202. const mockTemplateMap = mockTemplateList.reduce(
  2203. (acc, item) => {
  2204. const templateId = normalizeTemplateId(item.id);
  2205. if (templateId) {
  2206. acc[templateId] = item;
  2207. }
  2208. return acc;
  2209. },
  2210. {} as Record<number, TableInfo>,
  2211. );
  2212. const fixedTargetFields = computed(() => {
  2213. const template = templateDetail.value;
  2214. const activeNode = activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
  2215. const tableRules = normalizeTableRuleList(activeNode?.tableRuleList);
  2216. if (!template && !selectedTemplateId.value && !tableRules.length) return [] as MappingTargetField[];
  2217. const baseFields = tableRules.length
  2218. ? buildTargetFieldsFromRuleList(tableRules)
  2219. : buildTargetFieldsFromTemplateInfo(template);
  2220. return applyTableRuleRequiredFlags(baseFields, tableRules);
  2221. });
  2222. const sortedMappingTargetFields = computed(() =>
  2223. fixedTargetFields.value
  2224. .map((item, index) => ({ item, index }))
  2225. .sort((a, b) => {
  2226. const aRequired = isMappingTargetRequired(a.item);
  2227. const bRequired = isMappingTargetRequired(b.item);
  2228. if (aRequired === bRequired) {
  2229. return a.index - b.index;
  2230. }
  2231. return aRequired ? -1 : 1;
  2232. })
  2233. .map(({ item }) => item),
  2234. );
  2235. const defaultRightTableHeaders = ['列A', '列B', '列C', '列D', '列E', '列F', '列G', '列H'];
  2236. const rightTableHeaders = ref([...defaultRightTableHeaders]);
  2237. const addedColumns = ref<string[]>([]);
  2238. const selectedAddedColumn = ref('');
  2239. const sourceTableRows = ref<Record<string, string>[]>([]);
  2240. const tablePreviewRows = ref<Record<string, string>[]>(JSON.parse(JSON.stringify(sourceTableRows.value)));
  2241. const rulesList = [{ name: '单列清洗' }, { name: '其他清洗' }];
  2242. type CleaningRuleKey = RulePipelineKey;
  2243. interface CleaningRuleItem {
  2244. name: string;
  2245. key: CleaningRuleKey;
  2246. show: boolean;
  2247. active: boolean;
  2248. }
  2249. interface SelectedRuleItem {
  2250. name: string;
  2251. key: CleaningRuleKey;
  2252. active: boolean;
  2253. enabled: boolean;
  2254. rules: Record<string, any>;
  2255. }
  2256. interface CleaningRuleExample {
  2257. title: string;
  2258. description: string;
  2259. rows: Array<{
  2260. label: string;
  2261. value: string;
  2262. }>;
  2263. }
  2264. interface PreviewColumnSnapshot {
  2265. colIndex: number | null;
  2266. selectedRowIndex: number | null;
  2267. selectedAddedColumn: string;
  2268. activeTargetKey: string;
  2269. }
  2270. type GroupingColumnDraft = {
  2271. header: string;
  2272. direction: string;
  2273. count: number | undefined;
  2274. title: string;
  2275. };
  2276. const ruleNameMap: Record<CleaningRuleKey, string> = {
  2277. ignoreLine: '忽略行',
  2278. dateConfig: '日期和时间',
  2279. extractData: '提取数据',
  2280. extractSegment: '分割提取',
  2281. timestampConversion: '时间戳转换',
  2282. removeSpecialCharacters: '移除特殊字符',
  2283. absoluteValue: '绝对值',
  2284. decimalConversion: '进制转换',
  2285. replaceValue: '条件替换',
  2286. groupingColumns: '组合列',
  2287. magnificationAdjustment: '倍数放大器',
  2288. yearComplete: '年份补全',
  2289. usePreviousRowData: '沿用上一行数据',
  2290. };
  2291. const ruleExampleMap: Partial<Record<CleaningRuleKey, CleaningRuleExample>> = {
  2292. ignoreLine: {
  2293. title: '忽略行',
  2294. description: '按行号跳过无需参与清洗和导入的数据行。',
  2295. rows: [
  2296. { label: '输入', value: '2,5,8' },
  2297. { label: '效果', value: '第 2、5、8 行不会参与后续清洗与导入' },
  2298. ],
  2299. },
  2300. dateConfig: {
  2301. title: '日期和时间',
  2302. description: '识别常见日期或时间文本,并统一转换成标准日期格式。',
  2303. rows: [
  2304. { label: '清洗前', value: '20260324153020' },
  2305. { label: '清洗后', value: '2026-03-24 15:30:20' },
  2306. ],
  2307. },
  2308. extractData: {
  2309. title: '提取数据',
  2310. description: '从字段值前后截取指定长度的内容。',
  2311. rows: [
  2312. { label: '清洗前', value: '13800138000' },
  2313. { label: '前 3 位', value: '138' },
  2314. { label: '后 4 位', value: '8000' },
  2315. ],
  2316. },
  2317. extractSegment: {
  2318. title: '分割提取',
  2319. description: '按分隔符拆分字段值,再提取指定段位。',
  2320. rows: [
  2321. { label: '清洗前', value: '四川省-成都市-高新区' },
  2322. { label: '分隔符', value: '-' },
  2323. { label: '清洗后', value: '成都市' },
  2324. ],
  2325. },
  2326. timestampConversion: {
  2327. title: '时间戳转换',
  2328. description: '自动识别秒级或毫秒级时间戳,并转换为日期时间。',
  2329. rows: [
  2330. { label: '清洗前', value: '1711209600' },
  2331. { label: '清洗后', value: '2024-03-24 00:00:00' },
  2332. ],
  2333. },
  2334. removeSpecialCharacters: {
  2335. title: '移除特殊字符',
  2336. description: '移除空格、换行或自定义特殊字符。',
  2337. rows: [
  2338. { label: '清洗前', value: '138 0013-8000' },
  2339. { label: '清洗后', value: '13800138000' },
  2340. ],
  2341. },
  2342. absoluteValue: {
  2343. title: '绝对值',
  2344. description: '将数值型字段统一转换为正数。',
  2345. rows: [
  2346. { label: '清洗前', value: '-20' },
  2347. { label: '清洗后', value: '20' },
  2348. ],
  2349. },
  2350. decimalConversion: {
  2351. title: '进制转换',
  2352. description: '按配置将数值从当前进制转换到目标进制。',
  2353. rows: [
  2354. { label: '清洗前', value: '56(10进制)' },
  2355. { label: '清洗后', value: '38(16进制)' },
  2356. ],
  2357. },
  2358. replaceValue: {
  2359. title: '条件替换',
  2360. description: '按包含或等于条件,将命中的文本替换为目标值。',
  2361. rows: [
  2362. { label: '包含', value: '未知开户地址 -> --开户地址' },
  2363. { label: '等于', value: '空 -> --' },
  2364. ],
  2365. },
  2366. groupingColumns: {
  2367. title: '组合列',
  2368. description: '将多个来源列按顺序截取并拼接为新列。',
  2369. rows: [
  2370. { label: '配置', value: '姓名前 1 位 + 手机号后 4 位' },
  2371. { label: '清洗后', value: '张2369' },
  2372. ],
  2373. },
  2374. magnificationAdjustment: {
  2375. title: '倍数放大器',
  2376. description: '将数值按配置倍数放大,适合金额或比例字段换算。',
  2377. rows: [
  2378. { label: '清洗前', value: '1.23' },
  2379. { label: '倍数', value: '100' },
  2380. { label: '清洗后', value: '123' },
  2381. ],
  2382. },
  2383. yearComplete: {
  2384. title: '年份补全',
  2385. description: '当日期缺少年份时,按当前年份自动补全。',
  2386. rows: [
  2387. { label: '清洗前', value: '02-23 12:00:00' },
  2388. { label: '清洗后', value: '2026-02-23 12:00:00' },
  2389. ],
  2390. },
  2391. };
  2392. const disabledCleaningRuleKeySet = new Set<CleaningRuleKey>([
  2393. 'groupingColumns',
  2394. 'yearComplete',
  2395. 'removeSpecialCharacters',
  2396. 'usePreviousRowData',
  2397. ]);
  2398. const isCleaningRuleEnabledInUi = (key?: CleaningRuleKey | string | null): key is CleaningRuleKey =>
  2399. !!key && !disabledCleaningRuleKeySet.has(key as CleaningRuleKey);
  2400. const sanitizeSelectedRuleList = (rules?: SelectedRuleItem[] | null) =>
  2401. (Array.isArray(rules) ? rules : []).filter((rule) => isCleaningRuleEnabledInUi(rule?.key));
  2402. const sanitizeSelectedRuleGroups = (groups?: SelectedRuleItem[][] | null) =>
  2403. (Array.isArray(groups) ? groups : []).map((rules) => sanitizeSelectedRuleList(rules));
  2404. const cleaningRulesList = ref<CleaningRuleItem[]>([
  2405. { name: '忽略行', key: 'ignoreLine', show: false, active: false },
  2406. { name: '日期和时间', key: 'dateConfig', show: true, active: false },
  2407. { name: '提取数据', key: 'extractData', show: true, active: false },
  2408. { name: '分割提取', key: 'extractSegment', show: true, active: false },
  2409. { name: '时间戳转换', key: 'timestampConversion', show: true, active: false },
  2410. { name: '绝对值', key: 'absoluteValue', show: true, active: false },
  2411. { name: '进制转换', key: 'decimalConversion', show: true, active: false },
  2412. { name: '条件替换', key: 'replaceValue', show: true, active: false },
  2413. { name: '倍数放大器', key: 'magnificationAdjustment', show: true, active: false },
  2414. ]);
  2415. const activeRules = ref(0);
  2416. const isSharedRuleMode = computed(() => activeRules.value === 1);
  2417. const currentColumn = ref<number | null>(null);
  2418. const activeMappingTargetKey = ref('');
  2419. const selectedRowIndex = ref<number | null>(null);
  2420. const headerRowIndex = ref<number | null>(null);
  2421. const headerRowInput = ref('');
  2422. const tableMockRef = ref<HTMLElement | null>(null);
  2423. const generatedColumnHeaderRowMap = ref<Record<string, number | null>>({});
  2424. const selectRules = ref<SelectedRuleItem[][]>([]);
  2425. const selectedRules = ref<SelectedRuleItem[]>([]);
  2426. const currentRuleTitle = ref('');
  2427. const omitLineInput = ref('');
  2428. const dateConfigType = ref(1);
  2429. const extractDataMode = ref<'before' | 'after'>('before');
  2430. const extractDataBefore = ref('');
  2431. const extractDataAfter = ref('');
  2432. const extractSegmentValue = ref('');
  2433. const extractSegmentRow = ref<number | undefined>(1);
  2434. const absoluteValueChecked = ref(false);
  2435. const decimalEnabled = ref(false);
  2436. const decimalSourceBase = ref('10进制');
  2437. const decimalTargetBase = ref('16进制');
  2438. const replaceEnabled = ref(true);
  2439. const replaceMode = ref(0);
  2440. const replaceSource = ref('');
  2441. const replaceTarget = ref('');
  2442. const removeSpaceChecked = ref(false);
  2443. const removeTabChecked = ref(false);
  2444. const removeCustomChecked = ref(false);
  2445. const removeCustomText = ref('');
  2446. const groupingEnabled = ref(false);
  2447. const groupingColumnsDraft = ref<GroupingColumnDraft[]>(normalizeGroupingColumnsDraft());
  2448. const magnificationValue = ref<number | undefined>(1);
  2449. const syncExtractDataMode = () => {
  2450. extractDataMode.value = extractDataAfter.value && !extractDataBefore.value ? 'after' : 'before';
  2451. };
  2452. const handleExtractDataModeChange = (mode: 'before' | 'after') => {
  2453. extractDataMode.value = mode;
  2454. if (mode === 'before') {
  2455. extractDataAfter.value = '';
  2456. return;
  2457. }
  2458. extractDataBefore.value = '';
  2459. };
  2460. const dateTypeOptions = [
  2461. { label: '日期和时间', value: 1 },
  2462. { label: '日期', value: 2 },
  2463. { label: '时间', value: 3 },
  2464. ];
  2465. const decimalBaseOptions = ['2进制', '8进制', '10进制', '16进制'].map((item) => ({
  2466. label: item,
  2467. value: item,
  2468. }));
  2469. const replaceModeOptions = [
  2470. { label: '包含', value: 0 },
  2471. { label: '等于', value: 1 },
  2472. ];
  2473. function createGroupingColumnDraft(item?: Partial<GroupingColumnDraft> | Record<string, any>): GroupingColumnDraft {
  2474. const header = String(item?.header ?? '').trim();
  2475. const rawCount = item?.count;
  2476. const count =
  2477. rawCount === null || rawCount === undefined || rawCount === ''
  2478. ? undefined
  2479. : Number.isFinite(Number(rawCount))
  2480. ? Number(rawCount)
  2481. : undefined;
  2482. return {
  2483. header,
  2484. direction: String(item?.direction ?? '').trim(),
  2485. count,
  2486. title: String(item?.title ?? header).trim(),
  2487. };
  2488. }
  2489. function normalizeGroupingColumnsDraft(
  2490. list?: Array<Partial<GroupingColumnDraft> | Record<string, any>> | null,
  2491. ): GroupingColumnDraft[] {
  2492. const normalized = Array.isArray(list) ? list.map((item) => createGroupingColumnDraft(item)) : [];
  2493. return normalized.length ? normalized : [createGroupingColumnDraft()];
  2494. }
  2495. type NodeCacheState = {
  2496. selectedTemplateId: number;
  2497. fieldMapping: Record<string, string>;
  2498. rightTableHeaders: string[];
  2499. addedColumns: string[];
  2500. selectedAddedColumn: string;
  2501. sourceTableRows: Record<string, string>[];
  2502. selectRules: SelectedRuleItem[][];
  2503. selectedRules: SelectedRuleItem[];
  2504. activeRules: number;
  2505. currentColumn: number | null;
  2506. selectedRowIndex: number | null;
  2507. headerRowIndex: number | null;
  2508. headerRowInput: string;
  2509. currentRuleTitle: string;
  2510. omitLineInput: string;
  2511. dateConfigType: number;
  2512. extractDataBefore: string;
  2513. extractDataAfter: string;
  2514. extractSegmentValue: string;
  2515. extractSegmentRow: number | undefined;
  2516. absoluteValueChecked: boolean;
  2517. decimalEnabled: boolean;
  2518. decimalSourceBase: string;
  2519. decimalTargetBase: string;
  2520. replaceEnabled: boolean;
  2521. replaceMode: number;
  2522. replaceSource: string;
  2523. replaceTarget: string;
  2524. removeSpaceChecked: boolean;
  2525. removeTabChecked: boolean;
  2526. removeCustomChecked: boolean;
  2527. removeCustomText: string;
  2528. groupingEnabled: boolean;
  2529. groupingColumnsDraft: GroupingColumnDraft[];
  2530. magnificationValue: number | undefined;
  2531. addedColumnConfig: Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>;
  2532. splitColumnConfig: Record<
  2533. string,
  2534. { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
  2535. >;
  2536. generatedColumnHeaderRowMap: Record<string, number | null>;
  2537. recognitionDirectionFields: RecognitionDirectionField[];
  2538. recognitionBasicInfoBaseFuncRegx: string;
  2539. recognitionBasicInfoManualVisible: boolean;
  2540. recognitionBasicInfoExpanded: boolean;
  2541. recognitionBasicInfoRows: RecognitionBasicInfoRow[];
  2542. recognitionAmountZfEnabled: boolean;
  2543. childTemplateName: string;
  2544. };
  2545. const MAX_NODE_CACHE_SIZE = 20;
  2546. const nodeStateCache = new Map<string, NodeCacheState>();
  2547. const initialNodeStatusSignatureMap = new Map<string, string>();
  2548. const analyzingNodeKeys = ref<string[]>([]);
  2549. const cloneDeepSafe = <T,>(val: T): T => JSON.parse(JSON.stringify(val));
  2550. const normalizeGeneratedColumnHeaderRowIndex = (value: unknown, rowCount: number) => {
  2551. const rowIndex = Number(value);
  2552. return Number.isInteger(rowIndex) && rowIndex >= 0 && rowIndex < rowCount ? rowIndex : null;
  2553. };
  2554. const buildGeneratedColumnHeaderRowMap = (
  2555. headers: string[] = addedColumns.value,
  2556. rows: Record<string, string>[] = sourceTableRows.value,
  2557. currentMap?: Record<string, number | null> | null,
  2558. fallbackRowIndex: number | null = headerRowIndex.value,
  2559. ) =>
  2560. headers.reduce(
  2561. (acc, header) => {
  2562. if (!header) return acc;
  2563. if (currentMap && Object.prototype.hasOwnProperty.call(currentMap, header)) {
  2564. acc[header] = normalizeGeneratedColumnHeaderRowIndex(currentMap[header], rows.length);
  2565. return acc;
  2566. }
  2567. const inferredRowIndex = rows.findIndex((row) => String(row?.[header] ?? '') === header);
  2568. acc[header] =
  2569. inferredRowIndex >= 0
  2570. ? inferredRowIndex
  2571. : normalizeGeneratedColumnHeaderRowIndex(fallbackRowIndex, rows.length);
  2572. return acc;
  2573. },
  2574. {} as Record<string, number | null>,
  2575. );
  2576. const getGeneratedColumnHeaderRowIndex = (columnName: string) =>
  2577. buildGeneratedColumnHeaderRowMap(
  2578. addedColumns.value.includes(columnName) ? [columnName] : [],
  2579. sourceTableRows.value,
  2580. generatedColumnHeaderRowMap.value,
  2581. )[columnName] ?? null;
  2582. const setNodeAnalyzing = (nodeKey: string, analyzing: boolean) => {
  2583. if (!nodeKey) return;
  2584. if (analyzing) {
  2585. if (!analyzingNodeKeys.value.includes(nodeKey)) {
  2586. analyzingNodeKeys.value = [...analyzingNodeKeys.value, nodeKey];
  2587. }
  2588. return;
  2589. }
  2590. if (analyzingNodeKeys.value.includes(nodeKey)) {
  2591. analyzingNodeKeys.value = analyzingNodeKeys.value.filter((item) => item !== nodeKey);
  2592. }
  2593. };
  2594. const touchNodeCache = (nodeKey: string, state: NodeCacheState) => {
  2595. if (nodeStateCache.has(nodeKey)) {
  2596. nodeStateCache.delete(nodeKey);
  2597. }
  2598. nodeStateCache.set(nodeKey, state);
  2599. if (nodeStateCache.size > MAX_NODE_CACHE_SIZE) {
  2600. const oldestKey = nodeStateCache.keys().next().value;
  2601. if (oldestKey) nodeStateCache.delete(oldestKey);
  2602. }
  2603. };
  2604. const buildNodeCacheState = (): NodeCacheState => ({
  2605. selectedTemplateId: selectedTemplateId.value,
  2606. fieldMapping: cloneDeepSafe(fieldMapping.value),
  2607. rightTableHeaders: [...rightTableHeaders.value],
  2608. addedColumns: [...addedColumns.value],
  2609. selectedAddedColumn: selectedAddedColumn.value,
  2610. sourceTableRows: cloneDeepSafe(sourceTableRows.value),
  2611. selectRules: sanitizeSelectedRuleGroups(cloneDeepSafe(selectRules.value)),
  2612. selectedRules: sanitizeSelectedRuleList(cloneDeepSafe(selectedRules.value)),
  2613. activeRules: activeRules.value,
  2614. currentColumn: currentColumn.value,
  2615. selectedRowIndex: selectedRowIndex.value,
  2616. headerRowIndex: headerRowIndex.value,
  2617. headerRowInput: headerRowInput.value,
  2618. currentRuleTitle: currentRuleTitle.value,
  2619. omitLineInput: omitLineInput.value,
  2620. dateConfigType: dateConfigType.value,
  2621. extractDataBefore: extractDataBefore.value,
  2622. extractDataAfter: extractDataAfter.value,
  2623. extractSegmentValue: extractSegmentValue.value,
  2624. extractSegmentRow: extractSegmentRow.value,
  2625. absoluteValueChecked: absoluteValueChecked.value,
  2626. decimalEnabled: decimalEnabled.value,
  2627. decimalSourceBase: decimalSourceBase.value,
  2628. decimalTargetBase: decimalTargetBase.value,
  2629. replaceEnabled: replaceEnabled.value,
  2630. replaceMode: replaceMode.value,
  2631. replaceSource: replaceSource.value,
  2632. replaceTarget: replaceTarget.value,
  2633. removeSpaceChecked: removeSpaceChecked.value,
  2634. removeTabChecked: removeTabChecked.value,
  2635. removeCustomChecked: removeCustomChecked.value,
  2636. removeCustomText: removeCustomText.value,
  2637. groupingEnabled: groupingEnabled.value,
  2638. groupingColumnsDraft: cloneDeepSafe(groupingColumnsDraft.value),
  2639. magnificationValue: magnificationValue.value,
  2640. addedColumnConfig: cloneDeepSafe(addedColumnConfig.value),
  2641. splitColumnConfig: cloneDeepSafe(splitColumnConfig.value),
  2642. generatedColumnHeaderRowMap: cloneDeepSafe(buildGeneratedColumnHeaderRowMap()),
  2643. recognitionDirectionFields: cloneDeepSafe(recognitionDirectionFields.value),
  2644. recognitionBasicInfoBaseFuncRegx: recognitionBasicInfoBaseFuncRegx.value,
  2645. recognitionBasicInfoManualVisible: recognitionBasicInfoManualVisible.value,
  2646. recognitionBasicInfoExpanded: recognitionBasicInfoExpanded.value,
  2647. recognitionBasicInfoRows: cloneDeepSafe(recognitionBasicInfoRows.value),
  2648. recognitionAmountZfEnabled: recognitionAmountZfEnabled.value,
  2649. childTemplateName: childTemplateName.value,
  2650. });
  2651. const buildNodeStatusSignature = (state: NodeCacheState) =>
  2652. JSON.stringify({
  2653. selectedTemplateId: state.selectedTemplateId,
  2654. fieldMapping: state.fieldMapping,
  2655. rightTableHeaders: state.rightTableHeaders,
  2656. addedColumns: state.addedColumns,
  2657. headerRowIndex: state.headerRowIndex,
  2658. selectRules: state.selectRules.map((rules) =>
  2659. rules.map((rule) => ({
  2660. key: rule.key,
  2661. name: rule.name,
  2662. enabled: rule.enabled,
  2663. rules: rule.rules,
  2664. })),
  2665. ),
  2666. selectedRules: state.selectedRules.map((rule) => ({
  2667. key: rule.key,
  2668. name: rule.name,
  2669. enabled: rule.enabled,
  2670. rules: rule.rules,
  2671. })),
  2672. addedColumnConfig: state.addedColumnConfig,
  2673. splitColumnConfig: state.splitColumnConfig,
  2674. generatedColumnHeaderRowMap: state.generatedColumnHeaderRowMap,
  2675. recognitionDirectionFields: (state.recognitionDirectionFields || []).map((item) => ({
  2676. key: item.key,
  2677. label: item.label,
  2678. tab: item.tab,
  2679. entries: item.entries.map((entry) => ({
  2680. label: entry.label,
  2681. value: entry.value,
  2682. })),
  2683. })),
  2684. recognitionBasicInfoBaseFuncRegx: state.recognitionBasicInfoBaseFuncRegx,
  2685. recognitionBasicInfoManualVisible: state.recognitionBasicInfoManualVisible,
  2686. recognitionBasicInfoRows: state.recognitionBasicInfoRows.map((item) => ({
  2687. headerMatch: item.headerMatch,
  2688. targetField: item.targetField,
  2689. endFlag: item.endFlag,
  2690. })),
  2691. recognitionAmountZfEnabled: state.recognitionAmountZfEnabled,
  2692. childTemplateName: state.childTemplateName,
  2693. });
  2694. const applyNodeCacheState = (state: NodeCacheState) => {
  2695. selectedTemplateId.value = state.selectedTemplateId;
  2696. fieldMapping.value = cloneDeepSafe(state.fieldMapping);
  2697. rightTableHeaders.value = [...state.rightTableHeaders];
  2698. addedColumns.value = [...state.addedColumns];
  2699. selectedAddedColumn.value = state.selectedAddedColumn;
  2700. sourceTableRows.value = cloneDeepSafe(state.sourceTableRows);
  2701. selectRules.value = sanitizeSelectedRuleGroups(cloneDeepSafe(state.selectRules));
  2702. selectedRules.value = sanitizeSelectedRuleList(cloneDeepSafe(state.selectedRules));
  2703. activeRules.value = state.activeRules;
  2704. currentColumn.value = state.currentColumn;
  2705. selectedRowIndex.value = state.selectedRowIndex;
  2706. headerRowIndex.value = state.headerRowIndex;
  2707. headerRowInput.value = state.headerRowInput;
  2708. currentRuleTitle.value = state.currentRuleTitle;
  2709. omitLineInput.value = state.omitLineInput;
  2710. dateConfigType.value = state.dateConfigType;
  2711. extractDataBefore.value = state.extractDataBefore;
  2712. extractDataAfter.value = state.extractDataAfter;
  2713. syncExtractDataMode();
  2714. extractSegmentValue.value = state.extractSegmentValue;
  2715. extractSegmentRow.value = state.extractSegmentRow;
  2716. absoluteValueChecked.value = state.absoluteValueChecked;
  2717. decimalEnabled.value = state.decimalEnabled;
  2718. decimalSourceBase.value = state.decimalSourceBase;
  2719. decimalTargetBase.value = state.decimalTargetBase;
  2720. replaceEnabled.value = state.replaceEnabled;
  2721. replaceMode.value = state.replaceMode;
  2722. replaceSource.value = state.replaceSource;
  2723. replaceTarget.value = state.replaceTarget;
  2724. removeSpaceChecked.value = state.removeSpaceChecked;
  2725. removeTabChecked.value = state.removeTabChecked;
  2726. removeCustomChecked.value = state.removeCustomChecked;
  2727. removeCustomText.value = state.removeCustomText;
  2728. groupingEnabled.value = state.groupingEnabled;
  2729. groupingColumnsDraft.value = normalizeGroupingColumnsDraft(cloneDeepSafe(state.groupingColumnsDraft));
  2730. magnificationValue.value = state.magnificationValue;
  2731. addedColumnConfig.value = cloneDeepSafe(state.addedColumnConfig);
  2732. splitColumnConfig.value = cloneDeepSafe(state.splitColumnConfig);
  2733. generatedColumnHeaderRowMap.value = buildGeneratedColumnHeaderRowMap(
  2734. state.addedColumns,
  2735. state.sourceTableRows,
  2736. state.generatedColumnHeaderRowMap,
  2737. state.headerRowIndex,
  2738. );
  2739. recognitionDirectionFields.value = cloneDeepSafe(state.recognitionDirectionFields || []);
  2740. recognitionBasicInfoBaseFuncRegx.value = state.recognitionBasicInfoBaseFuncRegx;
  2741. recognitionBasicInfoManualVisible.value = state.recognitionBasicInfoManualVisible;
  2742. recognitionBasicInfoExpanded.value = state.recognitionBasicInfoExpanded;
  2743. recognitionBasicInfoRows.value = cloneDeepSafe(state.recognitionBasicInfoRows);
  2744. recognitionAmountZfEnabled.value = !!state.recognitionAmountZfEnabled;
  2745. childTemplateName.value = state.childTemplateName;
  2746. if (!selectedRules.value.some((item) => item.active) && currentColumn.value !== null) {
  2747. const currentColumnRules = selectRules.value[currentColumn.value] || [];
  2748. if (!currentColumnRules.some((item) => item.active)) {
  2749. currentRuleTitle.value = '';
  2750. }
  2751. }
  2752. runRulePipeline();
  2753. };
  2754. const resetNodeWorkState = () => {
  2755. selectedTemplateId.value = 0;
  2756. templateDetail.value = null;
  2757. templateDetailFromMock.value = false;
  2758. activeTemplateItem.value = null;
  2759. fieldMapping.value = {};
  2760. activeMappingTargetKey.value = '';
  2761. rightTableHeaders.value = [];
  2762. addedColumns.value = [];
  2763. selectedAddedColumn.value = '';
  2764. sourceTableRows.value = [];
  2765. selectRules.value = [];
  2766. selectedRules.value = [];
  2767. activeRules.value = 0;
  2768. currentColumn.value = null;
  2769. selectedRowIndex.value = null;
  2770. headerRowIndex.value = null;
  2771. headerRowInput.value = '';
  2772. currentRuleTitle.value = '';
  2773. omitLineInput.value = '';
  2774. dateConfigType.value = 1;
  2775. extractDataMode.value = 'before';
  2776. extractDataBefore.value = '';
  2777. extractDataAfter.value = '';
  2778. extractSegmentValue.value = '';
  2779. extractSegmentRow.value = 1;
  2780. absoluteValueChecked.value = false;
  2781. decimalEnabled.value = false;
  2782. decimalSourceBase.value = '10进制';
  2783. decimalTargetBase.value = '16进制';
  2784. replaceEnabled.value = true;
  2785. replaceMode.value = 0;
  2786. replaceSource.value = '';
  2787. replaceTarget.value = '';
  2788. removeSpaceChecked.value = false;
  2789. removeTabChecked.value = false;
  2790. removeCustomChecked.value = false;
  2791. removeCustomText.value = '';
  2792. groupingEnabled.value = false;
  2793. groupingColumnsDraft.value = normalizeGroupingColumnsDraft();
  2794. magnificationValue.value = 1;
  2795. addedColumnConfig.value = {};
  2796. splitColumnConfig.value = {};
  2797. generatedColumnHeaderRowMap.value = {};
  2798. recognitionDirectionFields.value = [];
  2799. recognitionBasicInfoBaseFuncRegx.value = '';
  2800. recognitionBasicInfoManualVisible.value = false;
  2801. recognitionBasicInfoExpanded.value = false;
  2802. recognitionBasicInfoRows.value = [];
  2803. recognitionAmountZfEnabled.value = false;
  2804. childTemplateName.value = '';
  2805. runRulePipeline();
  2806. };
  2807. const clearActiveNodeTemplateState = () => {
  2808. const node = activeNodeInfo.value;
  2809. const key = activeNodeKey.value;
  2810. if (!node || !key) return;
  2811. node.templateId = null;
  2812. node.mainId = null;
  2813. node.tableName = null;
  2814. node.tableRuleList = [];
  2815. node.childTemplateName = null;
  2816. node.funcRegx = null;
  2817. nodeStateCache.delete(key);
  2818. };
  2819. const saveCurrentNodeState = () => {
  2820. const key = activeNodeKey.value;
  2821. if (!key) return;
  2822. syncActiveNodeStructure();
  2823. touchNodeCache(key, buildNodeCacheState());
  2824. };
  2825. const switchNodeState = async (nextKey: string) => {
  2826. if (!nextKey) return;
  2827. if (activeNodeKey.value === nextKey) return;
  2828. saveCurrentNodeState();
  2829. setNodeAnalyzing(nextKey, true);
  2830. activeNodeKey.value = nextKey;
  2831. const currentNode = findTreeNodeByKey(treeData.value, nextKey)?.raw || null;
  2832. structureSyncSuspended.value = true;
  2833. try {
  2834. const cached = nodeStateCache.get(nextKey);
  2835. if (cached) {
  2836. applyNodeCacheState(cached);
  2837. await loadTemplateDetail(cached.selectedTemplateId, { silent: true, requestDetail: false });
  2838. touchNodeCache(nextKey, cached);
  2839. return;
  2840. }
  2841. resetNodeWorkState();
  2842. const shouldLoadNodeTemplate = !isTemplateMatchedFailStatus(currentNode?.fileStatus);
  2843. await loadTemplateDetail(shouldLoadNodeTemplate ? currentNode?.templateId : undefined, {
  2844. silent: true,
  2845. requestDetail: false,
  2846. });
  2847. await applyNodePreset(currentNode);
  2848. const initialState = buildNodeCacheState();
  2849. touchNodeCache(nextKey, initialState);
  2850. if (!initialNodeStatusSignatureMap.has(nextKey)) {
  2851. initialNodeStatusSignatureMap.set(nextKey, buildNodeStatusSignature(initialState));
  2852. }
  2853. } finally {
  2854. structureSyncSuspended.value = false;
  2855. syncActiveNodeStructure();
  2856. setNodeAnalyzing(nextKey, false);
  2857. }
  2858. };
  2859. const defaultRuleConfig = (): Record<string, any> => ({
  2860. segment: '',
  2861. segmentRow: 1,
  2862. beforeModel: '',
  2863. nextModel: '',
  2864. timeStamp: false,
  2865. dateType: 1,
  2866. specialList: [],
  2867. specialStr: '',
  2868. absoluteData: false,
  2869. decimalData: false,
  2870. decimalModel: '16进制',
  2871. decimalModel2: '10进制',
  2872. replaceFlag: true,
  2873. sourceData: '',
  2874. targetData: '',
  2875. replaceModel: 0,
  2876. omitLine: '',
  2877. groupFlag: false,
  2878. groupColumns: normalizeGroupingColumnsDraft(),
  2879. magnificationAdjustmentValue: 1,
  2880. });
  2881. const structureSyncSuspended = ref(false);
  2882. const normalizeStoredLineNoToRowIndex = (value?: string | number | null) => {
  2883. const lineNo = Number(value);
  2884. if (!Number.isInteger(lineNo)) return null;
  2885. return lineNo <= 0 ? 0 : lineNo - 1;
  2886. };
  2887. const buildStoredLineNo = (rowIndex: number | null) => {
  2888. if (rowIndex === null || rowIndex < 0) return null;
  2889. return rowIndex + 1;
  2890. };
  2891. const toRuleBoolean = (value: unknown, defaultValue = false) => {
  2892. if (value === null || value === undefined || value === '') {
  2893. return defaultValue;
  2894. }
  2895. if (typeof value === 'string') {
  2896. return value !== 'false';
  2897. }
  2898. return Boolean(value);
  2899. };
  2900. const persistFileListToSessionStorage = () => {
  2901. try {
  2902. sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizeFileInfoTree(fileList.value)));
  2903. } catch (error) {
  2904. // 忽略序列化异常,避免影响当前清洗编辑流程。
  2905. }
  2906. };
  2907. const createPreviewRowsByHeaders = (headers: string[]) => {
  2908. return headers.length ? [] : [];
  2909. };
  2910. const applyPreviewDataset = (dataset: PreviewFileDataset) => {
  2911. rightTableHeaders.value = [...dataset.headers];
  2912. addedColumns.value = [];
  2913. generatedColumnHeaderRowMap.value = {};
  2914. selectedAddedColumn.value = '';
  2915. sourceTableRows.value = dataset.rows.length ? dataset.rows : [];
  2916. tablePreviewRows.value = cloneDeepSafe(sourceTableRows.value);
  2917. return true;
  2918. };
  2919. type PreviewStateSnapshot = PreviewFileDataset & {
  2920. headerRowIndex: number | null;
  2921. selectedRowIndex: number | null;
  2922. };
  2923. const pickPreviewRowByHeaders = (row: Record<string, string>, headers: string[]) =>
  2924. headers.reduce(
  2925. (acc, header) => {
  2926. acc[header] = String(row?.[header] ?? '');
  2927. return acc;
  2928. },
  2929. {} as Record<string, string>,
  2930. );
  2931. const captureSourcePreviewSnapshot = (): PreviewStateSnapshot | null => {
  2932. const addedHeaderSet = new Set(addedColumns.value);
  2933. const headers = rightTableHeaders.value.filter((header) => !addedHeaderSet.has(header));
  2934. if (!headers.length) return null;
  2935. return {
  2936. headers,
  2937. rows: sourceTableRows.value.map((row) => pickPreviewRowByHeaders(row, headers)),
  2938. headerRowIndex: headerRowIndex.value,
  2939. selectedRowIndex: selectedRowIndex.value,
  2940. };
  2941. };
  2942. const restorePreviewSnapshot = (snapshot: PreviewStateSnapshot) => {
  2943. applyPreviewDataset({
  2944. headers: snapshot.headers,
  2945. rows: snapshot.rows,
  2946. });
  2947. headerRowIndex.value = snapshot.headerRowIndex;
  2948. selectedRowIndex.value = snapshot.selectedRowIndex;
  2949. headerRowInput.value = snapshot.headerRowIndex === null ? '' : String(snapshot.headerRowIndex + 1);
  2950. };
  2951. const loadPreviewDatasetFromFile = async (item?: TreeFileInfo | null): Promise<PreviewFileDataset | null> => {
  2952. // 从 DuckDB 动态表 raw_{sheetId} 查询预览数据(sheet 节点 id 即 sheetId),
  2953. // 替代原 Electron 端本地文件读取
  2954. if (!isTreeSheetNode(item) || item?.id === null || item?.id === undefined) return null;
  2955. try {
  2956. const payload = (await rawPreview({
  2957. sheetId: item.id,
  2958. headers: normalizeTemplateHeaders(item?.header),
  2959. headerRowIndex: normalizeStoredLineNoToRowIndex(item?.lineNo),
  2960. limit: 2000,
  2961. })) as PreviewFileDataset | null;
  2962. if (!payload?.headers?.length) return null;
  2963. return {
  2964. headers: payload.headers.map((header) => normalizeRuleText(header)).filter(Boolean),
  2965. rows: Array.isArray(payload.rows) ? payload.rows : [],
  2966. };
  2967. } catch (error) {
  2968. console.error('读取预览数据失败', item?.id, error);
  2969. return null;
  2970. }
  2971. };
  2972. const applyPresetHeadersFromNode = (item?: TreeFileInfo | null) => {
  2973. const headers = normalizeTemplateHeaders(item?.header);
  2974. if (!headers.length) return false;
  2975. return applyPreviewDataset({
  2976. headers,
  2977. rows: [],
  2978. });
  2979. };
  2980. const insertHeaderAt = (header: string, targetIndex?: number | null) => {
  2981. if (!header) return;
  2982. if (!rightTableHeaders.value.includes(header)) {
  2983. const maxIndex = rightTableHeaders.value.length;
  2984. const nextIndex =
  2985. typeof targetIndex === 'number' && Number.isInteger(targetIndex)
  2986. ? Math.min(Math.max(targetIndex, 0), maxIndex)
  2987. : maxIndex;
  2988. rightTableHeaders.value.splice(nextIndex, 0, header);
  2989. }
  2990. if (!addedColumns.value.includes(header)) {
  2991. addedColumns.value.push(header);
  2992. }
  2993. if (!Object.prototype.hasOwnProperty.call(generatedColumnHeaderRowMap.value, header)) {
  2994. generatedColumnHeaderRowMap.value[header] = headerRowIndex.value;
  2995. }
  2996. sourceTableRows.value = sourceTableRows.value.map((row) => ({
  2997. ...row,
  2998. [header]: String(row[header] ?? ''),
  2999. }));
  3000. };
  3001. const resolveStoredRuleKey = (rule?: TreeStoredFunRule | null) => {
  3002. const directKey = normalizeRuleText(rule?.ruleKey) as CleaningRuleKey;
  3003. if (
  3004. directKey &&
  3005. Object.prototype.hasOwnProperty.call(ruleNameMap, directKey) &&
  3006. isCleaningRuleEnabledInUi(directKey)
  3007. ) {
  3008. return directKey;
  3009. }
  3010. const legacyKey = normalizeRuleText(rule?.key) as CleaningRuleKey;
  3011. if (
  3012. legacyKey &&
  3013. Object.prototype.hasOwnProperty.call(ruleNameMap, legacyKey) &&
  3014. isCleaningRuleEnabledInUi(legacyKey)
  3015. ) {
  3016. return legacyKey;
  3017. }
  3018. const className = normalizeRuleText(rule?.['@class']);
  3019. if (className.endsWith('FunReplace')) return 'replaceValue';
  3020. if (className.endsWith('FunMultiplier')) return 'magnificationAdjustment';
  3021. if (className.endsWith('FunAbs')) return 'absoluteValue';
  3022. if (className.endsWith('FunRadix')) return 'decimalConversion';
  3023. if (className.endsWith('FunSpExtra')) return 'extractSegment';
  3024. if (className.endsWith('FunExtra')) return 'extractData';
  3025. if (className.endsWith('FunDateTime'))
  3026. return legacyKey === 'timestampConversion' ? 'timestampConversion' : 'dateConfig';
  3027. if (className.endsWith('FunDate') || className.endsWith('FunTime')) return 'dateConfig';
  3028. if (className.endsWith('FunRegular')) return 'removeSpecialCharacters';
  3029. return null;
  3030. };
  3031. const deserializeStoredRule = (rule?: TreeStoredFunRule | null): SelectedRuleItem | null => {
  3032. const key = resolveStoredRuleKey(rule);
  3033. if (!key) return null;
  3034. const rules = {
  3035. ...defaultRuleConfig(),
  3036. ...(cloneDeepSafe(rule?.rules || {}) as Record<string, any>),
  3037. };
  3038. const className = normalizeRuleText(rule?.['@class']);
  3039. if (key === 'replaceValue') {
  3040. rules.sourceData = String(rule?.target ?? rules.sourceData ?? '');
  3041. rules.targetData = String(rule?.replacement ?? rules.targetData ?? '');
  3042. }
  3043. if (key === 'magnificationAdjustment') {
  3044. rules.magnificationAdjustmentValue = Number(rule?.factor ?? rules.magnificationAdjustmentValue ?? 1) || 1;
  3045. }
  3046. if (key === 'extractSegment') {
  3047. rules.segment = String(rule?.key ?? rules.segment ?? '');
  3048. rules.segmentRow = Number(rule?.index ?? rules.segmentRow ?? 1) || 1;
  3049. }
  3050. if (key === 'extractData') {
  3051. const len = Number(rule?.len ?? 0) || 0;
  3052. const direction = normalizeRuleText(rule?.metaDirection) || (rule?.opened === false ? 'after' : 'before');
  3053. if (direction === 'after') {
  3054. rules.nextModel = len ? String(len) : rules.nextModel;
  3055. } else {
  3056. rules.beforeModel = len ? String(len) : rules.beforeModel;
  3057. }
  3058. }
  3059. if (key === 'decimalConversion') {
  3060. if (!rules.decimalModel && !rules.decimalModel2) {
  3061. if (Number(rule?.radixType) === 0) {
  3062. rules.decimalModel = '16进制';
  3063. rules.decimalModel2 = '10进制';
  3064. }
  3065. if (Number(rule?.radixType) === 1) {
  3066. rules.decimalModel = '10进制';
  3067. rules.decimalModel2 = '16进制';
  3068. }
  3069. }
  3070. rules.decimalData = rule?.opened ?? rules.decimalData ?? false;
  3071. }
  3072. if (key === 'absoluteValue') {
  3073. rules.absoluteData = rule?.opened ?? rules.absoluteData ?? true;
  3074. }
  3075. if (key === 'dateConfig') {
  3076. if (className.endsWith('FunDate')) {
  3077. rules.dateType = 2;
  3078. } else if (className.endsWith('FunTime')) {
  3079. rules.dateType = 3;
  3080. } else {
  3081. rules.dateType = 1;
  3082. }
  3083. }
  3084. if (key === 'timestampConversion') {
  3085. rules.timeStamp = true;
  3086. }
  3087. if (key === 'removeSpecialCharacters') {
  3088. const regex = String(rule?.regex ?? '');
  3089. if (!Array.isArray(rules.specialList) || !rules.specialList.length) {
  3090. const parsedRegularRule = parseRemoveSpecialRegex(regex);
  3091. rules.specialList = parsedRegularRule.specialList;
  3092. rules.specialStr = parsedRegularRule.specialStr;
  3093. }
  3094. }
  3095. return {
  3096. key,
  3097. name: normalizeRuleText(rule?.name) || ruleNameMap[key],
  3098. active: false,
  3099. enabled: true,
  3100. rules,
  3101. };
  3102. };
  3103. const buildStoredFunRules = (rule: SelectedRuleItem): TreeStoredFunRule[] => {
  3104. const rules = cloneDeepSafe(rule.rules || {});
  3105. const fallbackRule = {
  3106. ruleKey: rule.key,
  3107. rules,
  3108. };
  3109. if (rule.key === 'replaceValue') {
  3110. const fun = new FunReplace();
  3111. fun.target = String(rules.sourceData || '');
  3112. fun.replacement = String(rules.targetData || '');
  3113. return [buildStoredFunPayload(fun)];
  3114. }
  3115. if (rule.key === 'extractSegment') {
  3116. const fun = new FunSpExtra();
  3117. fun.key = String(rules.segment || '');
  3118. fun.index = Number(rules.segmentRow || 1) || 1;
  3119. return [buildStoredFunPayload(fun)];
  3120. }
  3121. if (rule.key === 'extractData') {
  3122. const nextRules: TreeStoredFunRule[] = [];
  3123. const beforeLen = Number(rules.beforeModel || 0) || 0;
  3124. const afterLen = Number(rules.nextModel || 0) || 0;
  3125. if (beforeLen > 0) {
  3126. const fun = new FunExtra();
  3127. fun.opened = true;
  3128. fun.len = beforeLen;
  3129. nextRules.push(buildStoredFunPayload(fun));
  3130. }
  3131. if (afterLen > 0) {
  3132. const fun = new FunExtra();
  3133. fun.opened = false;
  3134. fun.len = afterLen;
  3135. nextRules.push(buildStoredFunPayload(fun));
  3136. }
  3137. return nextRules.length ? nextRules : [fallbackRule];
  3138. }
  3139. if (rule.key === 'timestampConversion') {
  3140. const fun = new FunDateTime();
  3141. return [buildStoredFunPayload(fun, { ruleKey: 'timestampConversion' })];
  3142. }
  3143. if (rule.key === 'dateConfig') {
  3144. const dateType = Number(rules.dateType || 1) || 1;
  3145. const fun = dateType === 2 ? new FunDate() : dateType === 3 ? new FunTime() : new FunDateTime();
  3146. return [buildStoredFunPayload(fun)];
  3147. }
  3148. if (rule.key === 'removeSpecialCharacters') {
  3149. const fun = new FunRegular();
  3150. fun.regex = buildRemoveSpecialRegex(
  3151. Array.isArray(rules.specialList) ? rules.specialList : [],
  3152. String(rules.specialStr || ''),
  3153. );
  3154. return [buildStoredFunPayload(fun)];
  3155. }
  3156. if (rule.key === 'magnificationAdjustment') {
  3157. const fun = new FunMultiplier();
  3158. fun.factor = Number(rules.magnificationAdjustmentValue || 1) || 1;
  3159. return [buildStoredFunPayload(fun)];
  3160. }
  3161. if (rule.key === 'absoluteValue') {
  3162. const fun = new FunAbs();
  3163. fun.opened = toRuleBoolean(rules.absoluteData, true);
  3164. return [buildStoredFunPayload(fun)];
  3165. }
  3166. if (rule.key === 'decimalConversion') {
  3167. const targetBase = normalizeDecimalBaseLabel(rules.decimalModel);
  3168. const currentBase = normalizeDecimalBaseLabel(rules.decimalModel2);
  3169. if (currentBase === '10进制' && targetBase === '16进制') {
  3170. const fun = new FunRadix();
  3171. fun.radixType = 0;
  3172. fun.opened = toRuleBoolean(rules.decimalData, false);
  3173. return [buildStoredFunPayload(fun)];
  3174. }
  3175. if (currentBase === '16进制' && targetBase === '10进制') {
  3176. const fun = new FunRadix();
  3177. fun.radixType = 1;
  3178. fun.opened = toRuleBoolean(rules.decimalData, false);
  3179. return [buildStoredFunPayload(fun)];
  3180. }
  3181. return [fallbackRule];
  3182. }
  3183. return [fallbackRule];
  3184. };
  3185. const serializeStoredFunRules = (rulesList: SelectedRuleItem[], startSort = 0) => {
  3186. let currentSort = startSort;
  3187. return rulesList
  3188. .filter((rule) => rule.enabled && isCleaningRuleEnabledInUi(rule.key))
  3189. .flatMap((rule) =>
  3190. buildStoredFunRules(rule).map((item) => ({
  3191. ...item,
  3192. sort: ++currentSort,
  3193. })),
  3194. );
  3195. };
  3196. const sanitizeStoredFunRulePayload = (
  3197. rule?: TreeStoredFunRule | null,
  3198. options?: { keepRuleKey?: boolean },
  3199. ): TreeStoredFunRule | null => {
  3200. if (!rule) return null;
  3201. const className = normalizeRuleText(rule?.['@class']);
  3202. const sort = Number(rule?.sort);
  3203. const withSort = (payload: TreeStoredFunRule) =>
  3204. Number.isInteger(sort) && sort > 0 ? { ...payload, sort } : payload;
  3205. const keepRuleKey = options?.keepRuleKey !== false;
  3206. if (className.endsWith('FunReplace')) {
  3207. const fun = new FunReplace();
  3208. fun.target = String(rule?.target ?? '');
  3209. fun.replacement = String(rule?.replacement ?? '');
  3210. return withSort(buildStoredFunPayload(fun));
  3211. }
  3212. if (className.endsWith('FunSpExtra')) {
  3213. const fun = new FunSpExtra();
  3214. fun.key = String(rule?.key ?? '');
  3215. fun.index = Number(rule?.index ?? 1) || 1;
  3216. return withSort(buildStoredFunPayload(fun));
  3217. }
  3218. if (className.endsWith('FunExtra')) {
  3219. const fun = new FunExtra();
  3220. fun.opened = rule?.opened !== false;
  3221. fun.len = Number(rule?.len ?? 0) || 0;
  3222. return withSort(buildStoredFunPayload(fun));
  3223. }
  3224. if (className.endsWith('FunDate')) {
  3225. return withSort(buildStoredFunPayload(new FunDate()));
  3226. }
  3227. if (className.endsWith('FunTime')) {
  3228. return withSort(buildStoredFunPayload(new FunTime()));
  3229. }
  3230. if (className.endsWith('FunDateTime')) {
  3231. const payload = buildStoredFunPayload(new FunDateTime());
  3232. if (keepRuleKey && normalizeRuleText(rule?.ruleKey) === 'timestampConversion') {
  3233. return withSort({ ...payload, ruleKey: 'timestampConversion' });
  3234. }
  3235. return withSort(payload);
  3236. }
  3237. if (className.endsWith('FunRegular')) {
  3238. const fun = new FunRegular();
  3239. fun.regex = String(rule?.regex ?? '');
  3240. return withSort(buildStoredFunPayload(fun));
  3241. }
  3242. if (className.endsWith('FunMultiplier')) {
  3243. const fun = new FunMultiplier();
  3244. fun.factor = Number(rule?.factor ?? 1) || 1;
  3245. return withSort(buildStoredFunPayload(fun));
  3246. }
  3247. if (className.endsWith('FunAbs')) {
  3248. const fun = new FunAbs();
  3249. fun.opened = rule?.opened !== false;
  3250. return withSort(buildStoredFunPayload(fun));
  3251. }
  3252. if (className.endsWith('FunRadix')) {
  3253. const fun = new FunRadix();
  3254. fun.radixType = Number(rule?.radixType ?? 1) || 1;
  3255. fun.opened = toRuleBoolean(rule?.opened, false);
  3256. return withSort(buildStoredFunPayload(fun));
  3257. }
  3258. return cloneDeepSafe(rule);
  3259. };
  3260. const sanitizeStoredColumnRulePayload = (rule?: TreeStoredColumnRule | null): TreeStoredColumnRule | null => {
  3261. if (!rule) return null;
  3262. const className = normalizeRuleText(rule?.['@class']);
  3263. const mode = normalizeRuleText(rule?.mode);
  3264. if (className.endsWith('RuleSplit') || mode === 'split') {
  3265. const splitRule = new RuleSplit();
  3266. const refIndex = Number(rule?.refIndex);
  3267. const splitNameIndex = Number(rule?.splitNameIndex);
  3268. const splitNameIndex2 = Number(rule?.splitNameIndex2);
  3269. const len = Number(rule?.len);
  3270. const mappingEnum = Number(rule?.mappingEnum);
  3271. splitRule.refIndex = Number.isInteger(refIndex) && refIndex >= 0 ? refIndex : undefined;
  3272. splitRule.mappingEnum = Number.isInteger(mappingEnum)
  3273. ? mappingEnum
  3274. : Number.isInteger(len) && len > 0
  3275. ? MappingEnum.SP_LEN
  3276. : MappingEnum.SP_CHAR;
  3277. splitRule.splitNameIndex = Number.isInteger(splitNameIndex) && splitNameIndex >= 0 ? splitNameIndex : undefined;
  3278. splitRule.splitNameIndex2 =
  3279. Number.isInteger(splitNameIndex2) && splitNameIndex2 >= 0 ? splitNameIndex2 : undefined;
  3280. splitRule.len = Number.isInteger(len) && len > 0 ? len : undefined;
  3281. splitRule.splitChar = String(rule?.splitChar ?? '') || undefined;
  3282. return buildStoredColumnRulePayload(splitRule);
  3283. }
  3284. if (className.endsWith('RuleMapping') || mode === 'ref' || mode === 'custom') {
  3285. const mappingRule = new RuleMapping();
  3286. mappingRule.refIndex = Array.isArray(rule?.refIndex)
  3287. ? rule.refIndex.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
  3288. : [];
  3289. mappingRule.mappingEnum = Number(rule?.mappingEnum ?? MappingEnum.BLANK);
  3290. mappingRule.content = String(rule?.content ?? '');
  3291. return buildStoredColumnRulePayload(mappingRule);
  3292. }
  3293. return cloneDeepSafe(rule);
  3294. };
  3295. const sanitizeTreeTableRuleInfo = (
  3296. rule: TreeTableRuleInfo,
  3297. options?: { keepRuleKey?: boolean },
  3298. ): TreeTableRuleInfo => {
  3299. return {
  3300. ...cloneDeepSafe(rule),
  3301. funList: Array.isArray(rule.funList)
  3302. ? rule.funList
  3303. .map((item) => sanitizeStoredFunRulePayload(item, options))
  3304. .filter((item): item is TreeStoredFunRule => Boolean(item))
  3305. : rule.funList,
  3306. columnRule: sanitizeStoredColumnRulePayload(rule.columnRule),
  3307. };
  3308. };
  3309. const sanitizeTreeFileInfo = (item: TreeFileInfo, options?: { keepRuleKey?: boolean }): TreeFileInfo => ({
  3310. ...cloneDeepSafe(item),
  3311. tableRuleList: Array.isArray(item.tableRuleList)
  3312. ? item.tableRuleList.map((rule) => sanitizeTreeTableRuleInfo(rule, options))
  3313. : item.tableRuleList,
  3314. children: Array.isArray(item.children)
  3315. ? item.children.map((child) => sanitizeTreeFileInfo(child, options))
  3316. : item.children,
  3317. });
  3318. const sanitizeFileInfoTree = (list: TreeFileInfo[], options?: { keepRuleKey?: boolean }) =>
  3319. list.map((item) => sanitizeTreeFileInfo(item, options));
  3320. const buildColumnRulePayload = (header: string): TreeStoredColumnRule | null => {
  3321. const splitConfig = splitColumnConfig.value[header];
  3322. if (splitConfig) {
  3323. const splitNameIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName1);
  3324. const splitNameIndex2 = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName2);
  3325. const splitMode = normalizeSplitColumnMode(splitConfig.delimiterType);
  3326. const splitValue = normalizeSplitColumnValue(splitConfig.delimiterType, splitConfig.delimiterValue);
  3327. const splitLength = resolveSplitColumnLength(splitConfig.delimiterType, splitConfig.delimiterValue);
  3328. const splitRule = new RuleSplit();
  3329. splitRule.refIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.source);
  3330. splitRule.mappingEnum = splitMode === 'length' ? MappingEnum.SP_LEN : MappingEnum.SP_CHAR;
  3331. splitRule.splitNameIndex = splitNameIndex >= 0 ? splitNameIndex : undefined;
  3332. splitRule.splitNameIndex2 = splitNameIndex2 >= 0 ? splitNameIndex2 : undefined;
  3333. splitRule.len = splitMode === 'length' && splitLength > 0 ? splitLength : undefined;
  3334. splitRule.splitChar = splitMode === 'special' && splitValue ? splitValue : undefined;
  3335. return buildStoredColumnRulePayload(splitRule);
  3336. }
  3337. const addedConfig = addedColumnConfig.value[header];
  3338. if (!addedConfig) return null;
  3339. const mappingRule = new RuleMapping();
  3340. mappingRule.refIndex = addedConfig.contentFields
  3341. .map((field) => rightTableHeaders.value.findIndex((item) => item === field))
  3342. .filter((index) => index >= 0);
  3343. mappingRule.mappingEnum =
  3344. addedConfig.mode === 'custom'
  3345. ? addedConfig.customContent
  3346. ? MappingEnum.NEW_DATA
  3347. : MappingEnum.BLANK
  3348. : mappingRule.refIndex.length
  3349. ? MappingEnum.REF
  3350. : MappingEnum.BLANK;
  3351. mappingRule.content = addedConfig.mode === 'custom' ? addedConfig.customContent || '' : '';
  3352. return buildStoredColumnRulePayload(mappingRule);
  3353. };
  3354. const restoreStoredColumnRules = (list?: TreeTableRuleInfo[] | null) => {
  3355. const headerSnapshot = [...rightTableHeaders.value];
  3356. const normalizedRules = normalizeTableRuleList(list);
  3357. const splitRuleGroups = normalizedRules.reduce((acc, rule) => {
  3358. const header = normalizeRuleText(rule.fileColName);
  3359. const columnRule = rule.columnRule;
  3360. const className = normalizeRuleText(columnRule?.['@class']);
  3361. const mode = normalizeRuleText(columnRule?.mode);
  3362. if (!header || (!className.endsWith('RuleSplit') && mode !== 'split')) {
  3363. return acc;
  3364. }
  3365. const fileColIndex = Number(rule.fileColIndex);
  3366. const identity = getSplitColumnRuleIdentity(columnRule);
  3367. const prev = acc.get(identity) || [];
  3368. prev.push({
  3369. header,
  3370. fileColIndex: Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : Number.MAX_SAFE_INTEGER,
  3371. });
  3372. acc.set(identity, prev);
  3373. return acc;
  3374. }, new Map<string, Array<{ header: string; fileColIndex: number }>>());
  3375. normalizedRules.forEach((rule) => {
  3376. const header = normalizeRuleText(rule.fileColName);
  3377. if (!header) return;
  3378. const columnRule = rule.columnRule;
  3379. const fileColIndex = Number(rule.fileColIndex);
  3380. const targetIndex = Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : undefined;
  3381. if (columnRule) {
  3382. insertHeaderAt(header, targetIndex);
  3383. }
  3384. const className = normalizeRuleText(columnRule?.['@class']);
  3385. const mode = normalizeRuleText(columnRule?.mode);
  3386. if (className.endsWith('RuleSplit') || mode === 'split') {
  3387. const refIndex = Number(columnRule?.refIndex);
  3388. const source =
  3389. Number.isInteger(refIndex) && refIndex >= 0
  3390. ? headerSnapshot[refIndex] || rightTableHeaders.value[refIndex] || ''
  3391. : '';
  3392. if (!source) return;
  3393. const splitChar = String(columnRule?.splitChar ?? '');
  3394. const splitLength = Number(columnRule?.len);
  3395. const splitMappingEnum = Number(columnRule?.mappingEnum);
  3396. const isLengthSplit =
  3397. splitMappingEnum === MappingEnum.SP_LEN ||
  3398. (!Number.isInteger(splitMappingEnum) && Number.isInteger(splitLength) && splitLength > 0);
  3399. const splitNameIndex = Number(columnRule?.splitNameIndex);
  3400. const splitNameIndex2 = Number(columnRule?.splitNameIndex2);
  3401. const groupedTargetItems = (splitRuleGroups.get(getSplitColumnRuleIdentity(columnRule)) || [])
  3402. .slice()
  3403. .sort((a, b) => a.fileColIndex - b.fileColIndex);
  3404. const groupedTargets = groupedTargetItems.map((item) => item.header).filter(Boolean);
  3405. const uniqueTargets = Array.from(new Set(groupedTargets));
  3406. const resolveGroupedTargetByIndex = (index: number) =>
  3407. Number.isInteger(index) && index >= 0
  3408. ? groupedTargetItems.find((item) => item.fileColIndex === index)?.header || ''
  3409. : '';
  3410. let targetName1 = resolveGroupedTargetByIndex(splitNameIndex);
  3411. let targetName2 = resolveGroupedTargetByIndex(splitNameIndex2);
  3412. if (!targetName1 && !targetName2) {
  3413. targetName1 = uniqueTargets[0] || header;
  3414. targetName2 = uniqueTargets.find((item) => item !== targetName1) || '';
  3415. }
  3416. if (targetName1 && targetName2 && targetName1 === targetName2) {
  3417. if (Number.isInteger(fileColIndex) && fileColIndex === splitNameIndex2) {
  3418. targetName1 = '';
  3419. } else {
  3420. targetName2 = '';
  3421. }
  3422. }
  3423. splitColumnConfig.value[header] = {
  3424. source,
  3425. delimiterType: isLengthSplit ? 'length' : 'special',
  3426. delimiterValue: isLengthSplit ? String(splitLength > 0 ? splitLength : '') : splitChar,
  3427. targetName1,
  3428. targetName2,
  3429. };
  3430. return;
  3431. }
  3432. if (className.endsWith('RuleMapping') || mode === 'ref' || mode === 'custom') {
  3433. const refIndexList = Array.isArray(columnRule?.refIndex)
  3434. ? columnRule.refIndex.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
  3435. : [];
  3436. const mappingEnum = Number(columnRule?.mappingEnum);
  3437. const isRefMapping = mappingEnum === MappingEnum.REF || (!Number.isFinite(mappingEnum) && mode === 'ref');
  3438. addedColumnConfig.value[header] = {
  3439. mode: isRefMapping ? 'ref' : 'custom',
  3440. contentFields: refIndexList
  3441. .map((index) => headerSnapshot[index] || rightTableHeaders.value[index])
  3442. .filter(Boolean),
  3443. customContent: String(columnRule?.content ?? ''),
  3444. };
  3445. }
  3446. });
  3447. };
  3448. const restoreStoredFunRules = (list?: TreeTableRuleInfo[] | null) => {
  3449. selectedRules.value = [];
  3450. selectRules.value = rightTableHeaders.value.map(() => []);
  3451. const sharedRuleKeySet = new Set<string>();
  3452. const mergeColumnRule = (
  3453. columnRules: SelectedRuleItem[],
  3454. nextRule: SelectedRuleItem,
  3455. rawRule: TreeStoredFunRule,
  3456. ) => {
  3457. if (nextRule.key !== 'extractData') {
  3458. columnRules.push(nextRule);
  3459. return;
  3460. }
  3461. const lastRule = columnRules[columnRules.length - 1];
  3462. if (!lastRule || lastRule.key !== 'extractData') {
  3463. columnRules.push(nextRule);
  3464. return;
  3465. }
  3466. const direction = normalizeRuleText(rawRule?.metaDirection) || (rawRule?.opened === false ? 'after' : 'before');
  3467. if (direction === 'after') {
  3468. lastRule.rules.nextModel = nextRule.rules.nextModel;
  3469. } else {
  3470. lastRule.rules.beforeModel = nextRule.rules.beforeModel;
  3471. }
  3472. };
  3473. normalizeTableRuleList(list).forEach((tableRule) => {
  3474. const header = normalizeRuleText(tableRule.fileColName);
  3475. const columnIndex = header ? rightTableHeaders.value.findIndex((item) => item === header) : -1;
  3476. const columnRules: SelectedRuleItem[] = [];
  3477. (tableRule.funList || [])
  3478. .slice()
  3479. .sort((a, b) => Number(a?.sort ?? 0) - Number(b?.sort ?? 0))
  3480. .forEach((rule) => {
  3481. const nextRule = deserializeStoredRule(rule);
  3482. if (!nextRule) return;
  3483. if (nextRule.key === 'ignoreLine') {
  3484. if (!sharedRuleKeySet.has(nextRule.key)) {
  3485. selectedRules.value.push(nextRule);
  3486. sharedRuleKeySet.add(nextRule.key);
  3487. }
  3488. return;
  3489. }
  3490. mergeColumnRule(columnRules, nextRule, rule);
  3491. });
  3492. if (columnIndex >= 0 && columnRules.length) {
  3493. selectRules.value[columnIndex] = sanitizeSelectedRuleList(columnRules);
  3494. }
  3495. });
  3496. selectedRules.value = sanitizeSelectedRuleList(selectedRules.value);
  3497. };
  3498. const restoreFileLevelRules = (item?: TreeFileInfo | null) => {
  3499. const ignoreLines = Array.isArray(item?.ignoreLines)
  3500. ? item.ignoreLines.map((line) => Number(line)).filter((line) => Number.isInteger(line) && line > 0)
  3501. : [];
  3502. if (ignoreLines.length) {
  3503. const existingIgnoreRule = selectedRules.value.find((rule) => rule.key === 'ignoreLine');
  3504. const nextIgnoreRules = {
  3505. ...defaultRuleConfig(),
  3506. ...(existingIgnoreRule?.rules || {}),
  3507. omitLine: ignoreLines.join(','),
  3508. };
  3509. if (existingIgnoreRule) {
  3510. existingIgnoreRule.rules = nextIgnoreRules;
  3511. existingIgnoreRule.enabled = true;
  3512. } else {
  3513. selectedRules.value = [
  3514. ...selectedRules.value,
  3515. {
  3516. name: ruleNameMap.ignoreLine,
  3517. key: 'ignoreLine',
  3518. active: selectedRules.value.length === 0,
  3519. enabled: true,
  3520. rules: nextIgnoreRules,
  3521. },
  3522. ];
  3523. }
  3524. }
  3525. };
  3526. const resolveTemplateFieldMeta = (field: MappingTargetField, index: number) => {
  3527. const tableFields = getOrderedTemplateFields(
  3528. templateDetail.value?.tableFields || activeTemplateItem.value?.tableFields,
  3529. );
  3530. return (
  3531. tableFields.find(
  3532. (item, itemIndex) =>
  3533. normalizeRuleText(getTemplateFieldTargetKey(item, itemIndex)) === field.key ||
  3534. normalizeRuleText(getTemplateFieldTargetLabel(item, itemIndex)) === field.label,
  3535. ) || null
  3536. );
  3537. };
  3538. const findTemplateFieldMetaByField = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
  3539. const tableFields = getOrderedTemplateFields(
  3540. templateDetail.value?.tableFields || activeTemplateItem.value?.tableFields,
  3541. );
  3542. return (
  3543. tableFields.find(
  3544. (item, index) =>
  3545. normalizeRuleText(getTemplateFieldTargetKey(item, index)) === field.key ||
  3546. normalizeRuleText(getTemplateFieldTargetLabel(item, index)) === normalizeRuleText(field.label),
  3547. ) || null
  3548. );
  3549. };
  3550. const buildTableRuleListFromState = (list?: TreeTableRuleInfo[] | null) => {
  3551. const previousRules = normalizeTableRuleList(list);
  3552. const currentTemplateTableId = normalizeTemplateId(
  3553. templateDetail.value?.id ??
  3554. activeTemplateItem.value?.id ??
  3555. selectedTemplateId.value ??
  3556. activeNodeInfo.value?.templateId,
  3557. );
  3558. const previousRuleMap = previousRules.reduce(
  3559. (acc, item, index) => {
  3560. const key = getTableRuleTargetKey(item, index);
  3561. if (key) acc[key] = item;
  3562. return acc;
  3563. },
  3564. {} as Record<string, TreeTableRuleInfo>,
  3565. );
  3566. const sharedFunRules = serializeStoredFunRules(selectedRules.value);
  3567. const directionConfMap = recognitionDirectionFields.value.reduce(
  3568. (acc, item) => {
  3569. acc[item.key] = buildRecognitionDirectionConf(item.entries);
  3570. return acc;
  3571. },
  3572. {} as Record<string, string>,
  3573. );
  3574. return fixedTargetFields.value.map((field, index) => {
  3575. const previous = previousRuleMap[field.key];
  3576. const requiredMarker = getTableRuleRequiredFlag(previous, field.required) || field.required;
  3577. const mappedHeader = normalizeRuleText(effectiveFieldMapping.value[field.key]);
  3578. const matchedByRecognitionRule = isTargetHandledByRecognitionRule(field.key);
  3579. const fileColIndex = mappedHeader ? rightTableHeaders.value.findIndex((item) => item === mappedHeader) : -1;
  3580. const columnFunRules =
  3581. fileColIndex >= 0 ? serializeStoredFunRules(selectRules.value[fileColIndex] || [], sharedFunRules.length) : [];
  3582. const templateField = resolveTemplateFieldMeta(field, index);
  3583. const templateFieldRecord = (templateField || {}) as Record<string, any>;
  3584. // 交易金额字段以「正负识别」开关为准(开关本身已从节点/模板的 zf 初始化);
  3585. // 其余字段沿用模板字段 / 上一条规则的 zf 并兜底 0,保证每条规则都带整数 zf
  3586. // ——后端 TableRuleDTO.zf 是 Integer,清洗器 `rule.getZf() == 1` 会自动拆箱,缺字段必 NPE。
  3587. const zf = isTransAmountTargetField(field)
  3588. ? recognitionAmountZfEnabled.value
  3589. ? 1
  3590. : 0
  3591. : resolveStoredZfFlag(templateFieldRecord, previous);
  3592. const requiredFlagValue = pickFirstDefinedRuleFlagValue(
  3593. getRawRuleFlagValue(templateFieldRecord, 'required'),
  3594. getRawRuleFlagValue(previous, 'required'),
  3595. requiredMarker ? 1 : 0,
  3596. );
  3597. const matchedFlagValue =
  3598. matchedByRecognitionRule || mappedHeader
  3599. ? 1
  3600. : pickFirstDefinedRuleFlagValue(
  3601. getRawRuleFlagValue(templateFieldRecord, 'matched'),
  3602. getRawRuleFlagValue(previous, 'matched'),
  3603. requiredMarker ? 1 : 0,
  3604. );
  3605. const matchedRequiredFlagValue = pickFirstDefinedRuleFlagValue(getRawRuleFlagValue(previous, 'matchedRequired'));
  3606. const directionConf =
  3607. directionConfMap[field.key] ||
  3608. normalizeRecognitionConfigText(previous?.directionConf) ||
  3609. normalizeRecognitionConfigText(templateFieldRecord.directionConf ?? templateFieldRecord.direction_conf);
  3610. const fileRuleList = [...(index === 0 ? sharedFunRules : []), ...columnFunRules];
  3611. return {
  3612. id: previous?.id ?? null,
  3613. tableId: currentTemplateTableId || normalizeTemplateId(previous?.tableId) || null,
  3614. fieldNameCn: field.label,
  3615. fieldNameEn: field.key,
  3616. fieldType: normalizeRuleText(templateField?.fieldType) || normalizeRuleText(previous?.fieldType) || 'string',
  3617. required: requiredFlagValue,
  3618. fieldLen: previous?.fieldLen ?? null,
  3619. matched: matchedFlagValue,
  3620. matchedRequired: matchedRequiredFlagValue,
  3621. directionConf: directionConf || null,
  3622. zf,
  3623. fieldSort: index + 1,
  3624. fileColIndex: fileColIndex >= 0 ? fileColIndex : null,
  3625. fileColName: mappedHeader || null,
  3626. funList: fileRuleList,
  3627. columnRule: mappedHeader ? buildColumnRulePayload(mappedHeader) : null,
  3628. } as TreeTableRuleInfo;
  3629. });
  3630. };
  3631. const syncNodeStructure = (node?: TreeFileInfo | null) => {
  3632. if (!isTreeSheetNode(node)) return;
  3633. const templateId = normalizeTemplateId(
  3634. templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value ?? node.templateId,
  3635. );
  3636. const mainId = getSelectedTemplateMainId(node.mainId);
  3637. const tableName =
  3638. normalizeRuleText(templateDetail.value?.tableNameCn) ||
  3639. normalizeRuleText(activeTemplateItem.value?.tableNameCn) ||
  3640. normalizeRuleText(node.tableName);
  3641. const baseHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
  3642. const existingHeaders = normalizeTemplateHeaders(node.header);
  3643. const hasRealHeaderConfig =
  3644. headerRowIndex.value !== null ||
  3645. !!existingHeaders.length ||
  3646. addedColumns.value.length > 0 ||
  3647. Object.values(effectiveFieldMapping.value).some(Boolean) ||
  3648. baseHeaders.length > 0;
  3649. const shouldPersistRecognitionBasicInfo = hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value);
  3650. const ignoreLines = Array.from(ignoredRowsSet.value).sort((a, b) => a - b);
  3651. const groupColumns = buildEnabledGroupingColumnsPayload();
  3652. node.templateId = templateId || node.templateId || null;
  3653. node.mainId = mainId || null;
  3654. node.tableName = tableName || node.tableName || null;
  3655. if (headerRowIndex.value !== null) {
  3656. node.lineNo = buildStoredLineNo(headerRowIndex.value);
  3657. }
  3658. if (hasRealHeaderConfig) {
  3659. node.header = baseHeaders.length ? [...baseHeaders] : [];
  3660. }
  3661. node.ignoreLines = ignoreLines;
  3662. node.groupColumns = groupColumns;
  3663. node.tableRuleList = buildTableRuleListFromState(node.tableRuleList);
  3664. node.childTemplateName = normalizeRuleText(childTemplateName.value) || null;
  3665. node.hasNew = !!node.childTemplateName;
  3666. if (shouldPersistRecognitionBasicInfo) {
  3667. node.funcRegx = buildRecognitionBasicInfoFuncRegx();
  3668. } else if (Object.prototype.hasOwnProperty.call(node, 'funcRegx')) {
  3669. node.funcRegx = null;
  3670. }
  3671. persistFileListToSessionStorage();
  3672. };
  3673. const syncActiveNodeStructure = () => {
  3674. if (structureSyncSuspended.value) return;
  3675. syncNodeStructure(activeNodeInfo.value);
  3676. };
  3677. const visibleCleaningRules = computed(() => cleaningRulesList.value.filter((item) => item.show));
  3678. const currentSelectedRules = computed(() => {
  3679. if (isSharedRuleMode.value) {
  3680. return sanitizeSelectedRuleList(selectedRules.value);
  3681. }
  3682. if (currentColumn.value === null) return [];
  3683. return sanitizeSelectedRuleList(selectRules.value[currentColumn.value] || []);
  3684. });
  3685. const currentActiveSelectedRule = computed(() => currentSelectedRules.value.find((item) => item.active));
  3686. const currentPickedRule = computed(() => visibleCleaningRules.value.find((item) => item.active));
  3687. const currentPickedRuleKey = computed<CleaningRuleKey | undefined>(
  3688. () => currentActiveSelectedRule.value?.key || currentPickedRule.value?.key,
  3689. );
  3690. const currentRuleExample = computed(() =>
  3691. currentPickedRuleKey.value ? ruleExampleMap[currentPickedRuleKey.value] : undefined,
  3692. );
  3693. const currentActiveRuleDisplayName = computed(() =>
  3694. currentPickedRuleKey.value ? ruleNameMap[currentPickedRuleKey.value] : '等待选择',
  3695. );
  3696. const getIgnoredRowsSetFromSelectedRules = (rules: SelectedRuleItem[] = []) => {
  3697. const activeIgnoreRule = rules.find((rule) => rule.key === 'ignoreLine' && rule.enabled);
  3698. const raw = String(activeIgnoreRule?.rules?.omitLine || '').trim();
  3699. const nums = raw
  3700. .split(',')
  3701. .map((s) => Number(s.trim()))
  3702. .filter((n) => Number.isInteger(n) && n > 0);
  3703. return new Set(nums);
  3704. };
  3705. const ignoredRowsSet = computed(() => getIgnoredRowsSetFromSelectedRules(selectedRules.value));
  3706. const currentColumnTitle = computed(() => {
  3707. if (currentColumn.value === null) return '';
  3708. return rightTableHeaders.value[currentColumn.value] || '';
  3709. });
  3710. const currentRuleEnabledLabel = computed(() => (currentActiveSelectedRule.value?.enabled ? '已启用' : '未启用'));
  3711. const currentRuleEnabledClass = computed(() =>
  3712. currentActiveSelectedRule.value?.enabled ? 'is-enabled' : 'is-disabled',
  3713. );
  3714. const currentRuleScopeType = computed(() => (isSharedRuleMode.value ? 'shared' : 'column'));
  3715. const currentRuleScopeLabel = computed(() => (currentRuleScopeType.value === 'shared' ? '共享规则' : '列级规则'));
  3716. const groupingHeaderOptions = computed(() =>
  3717. rightTableHeaders.value.map((header) => ({
  3718. label: header,
  3719. value: header,
  3720. })),
  3721. );
  3722. const isCurrentColumnAdded = computed(
  3723. () => !!currentColumnTitle.value && addedColumns.value.includes(currentColumnTitle.value),
  3724. );
  3725. const activeNodeTableRuleMap = computed(() =>
  3726. normalizeTableRuleList(findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw?.tableRuleList).reduce(
  3727. (acc, item, index) => {
  3728. const key = getTableRuleTargetKey(item, index);
  3729. if (key) {
  3730. acc[key] = item;
  3731. }
  3732. return acc;
  3733. },
  3734. {} as Record<string, TreeTableRuleInfo>,
  3735. ),
  3736. );
  3737. const findActiveNodeTableRule = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
  3738. const ruleByKey = activeNodeTableRuleMap.value[field.key];
  3739. if (ruleByKey) {
  3740. return ruleByKey;
  3741. }
  3742. const normalizedLabel = normalizeRuleText(field.label);
  3743. if (!normalizedLabel) {
  3744. return null;
  3745. }
  3746. return (
  3747. normalizeTableRuleList(findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw?.tableRuleList).find(
  3748. (item, index) => {
  3749. const ruleLabel = normalizeRuleText(getTableRuleTargetLabel(item, index));
  3750. return ruleLabel === normalizedLabel;
  3751. },
  3752. ) || null
  3753. );
  3754. };
  3755. const isMappingTargetRequired = (field: Pick<MappingTargetField, 'key' | 'label' | 'required'>) => {
  3756. const templateField = findTemplateFieldMetaByField(field);
  3757. return getTableRuleRequiredFlag(
  3758. findActiveNodeTableRule(field),
  3759. getTemplateFieldRequiredFlag(templateField, field.required),
  3760. );
  3761. };
  3762. const formatTypeLabelMap: Record<FormatValidationType, string> = {
  3763. number: '数值',
  3764. date: '日期',
  3765. dateTime: '日期时间',
  3766. time: '时间',
  3767. loanFlag: '借贷标志',
  3768. callType: '主被叫',
  3769. phone: '号码',
  3770. acctCardNo: '账号',
  3771. idCardNumber: '身份证号',
  3772. };
  3773. const normalizeFormatValidationType = (fieldType?: string | null, fieldLabel = ''): FormatValidationType | '' => {
  3774. const normalizedFieldType = normalizeRuleText(fieldType)
  3775. .replace(/[\s-]+/g, '_')
  3776. .toUpperCase();
  3777. if (['TEXT', 'STRING', 'CHAR', 'VARCHAR', 'LONGTEXT', 'CLOB', '1'].includes(normalizedFieldType)) {
  3778. return '';
  3779. }
  3780. if (['DATE_TIME', 'DATETIME', 'TIMESTAMP', '3'].includes(normalizedFieldType)) {
  3781. return 'dateTime';
  3782. }
  3783. if (['DATE', 'DATE_ONLY', 'DATEONLY', '2'].includes(normalizedFieldType)) {
  3784. return 'date';
  3785. }
  3786. if (['TIME', '6'].includes(normalizedFieldType)) {
  3787. return 'time';
  3788. }
  3789. if (['PHONE', 'MOBILE', 'TEL', 'TELEPHONE'].includes(normalizedFieldType)) {
  3790. return 'phone';
  3791. }
  3792. if (
  3793. [
  3794. 'ACCT_CARD_NO',
  3795. 'ACCOUNT',
  3796. 'ACCOUNT_NO',
  3797. 'ACCOUNT_NUMBER',
  3798. 'ACCT_NO',
  3799. 'CARD_NO',
  3800. 'BANK_CARD',
  3801. 'BANK_CARD_NO',
  3802. ].includes(normalizedFieldType)
  3803. ) {
  3804. return 'acctCardNo';
  3805. }
  3806. if (['DEBIT_CREDIT_FLAG', 'LOAN_FLAG'].includes(normalizedFieldType)) {
  3807. return 'loanFlag';
  3808. }
  3809. if (['CALL_DIRECTION', 'CALL_TYPE'].includes(normalizedFieldType)) {
  3810. return 'callType';
  3811. }
  3812. if (['ID_CARD_NUMBER', 'ID_CARD', 'IDENTITY_CARD', 'CERT_NO', 'CERTIFICATE_NO'].includes(normalizedFieldType)) {
  3813. return 'idCardNumber';
  3814. }
  3815. if (
  3816. [
  3817. 'NUMBER',
  3818. 'NUMERIC',
  3819. 'DECIMAL',
  3820. 'DOUBLE',
  3821. 'FLOAT',
  3822. 'INTEGER',
  3823. 'INT',
  3824. 'LONG',
  3825. 'BIGINT',
  3826. 'BIGDECIMAL',
  3827. 'AMOUNT',
  3828. 'MONEY',
  3829. '4',
  3830. '5',
  3831. ].includes(normalizedFieldType)
  3832. ) {
  3833. return 'number';
  3834. }
  3835. if (normalizedFieldType) {
  3836. return '';
  3837. }
  3838. const normalizedLabel = normalizeFieldMatchText(fieldLabel);
  3839. if (!normalizedLabel) return '';
  3840. if (/(号码状态|手机状态|手机号状态|电话状态)/.test(normalizedLabel)) {
  3841. return '';
  3842. }
  3843. if (
  3844. /(电话|联系电话|手机|手机号|手机号码|电话号码|本方号码|对方号码|主叫号码|被叫号码|号码)/.test(normalizedLabel) &&
  3845. !/(类别|类型|归属地|地区|地址|编号|运单号|证件号|卡号|账号)/.test(normalizedLabel)
  3846. ) {
  3847. return 'phone';
  3848. }
  3849. if (
  3850. /(金额|数值|数量|次数|时长|单价|价格|余额|积分|额度|里程|公里|重量|身高|年龄|税额|总价)/.test(normalizedLabel)
  3851. ) {
  3852. return 'number';
  3853. }
  3854. if (normalizedLabel.includes('日期时间')) {
  3855. return 'dateTime';
  3856. }
  3857. if ((normalizedLabel.includes('日期') && normalizedLabel.includes('时间')) || normalizedLabel.includes('时刻')) {
  3858. return 'dateTime';
  3859. }
  3860. if (normalizedLabel.includes('时间')) {
  3861. return 'dateTime';
  3862. }
  3863. if (normalizedLabel.includes('日期') || normalizedLabel.includes('生日') || normalizedLabel.includes('出生')) {
  3864. return 'date';
  3865. }
  3866. return '';
  3867. };
  3868. const isLeapYear = (year: number) => (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  3869. const getMonthDayCount = (year: number, month: number) =>
  3870. [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] || 0;
  3871. const isValidDateParts = (year: number, month: number, day: number) =>
  3872. year >= 1000 && month >= 1 && month <= 12 && day >= 1 && day <= getMonthDayCount(year, month);
  3873. const isValidTimeParts = (hour: number, minute: number, second = 0, fraction = '') =>
  3874. hour >= 0 &&
  3875. hour <= 23 &&
  3876. minute >= 0 &&
  3877. minute <= 59 &&
  3878. second >= 0 &&
  3879. second <= 59 &&
  3880. (!fraction || /^\d{1,6}$/u.test(fraction));
  3881. const normalizeFormatValue = (value: unknown) =>
  3882. String(value ?? '')
  3883. .replace(/\u00a0/g, ' ')
  3884. .trim();
  3885. const normalizeDateLikeText = (value: string) => {
  3886. const normalized = normalizeFormatValue(value)
  3887. .replace(/(Z|[+-]\d{2}:?\d{2})$/iu, '')
  3888. .replace(/年/g, '-')
  3889. .replace(/月/g, '-')
  3890. .replace(/[日号]/g, ' ')
  3891. .replace(/[Tt]/g, ' ')
  3892. .replace(/\//g, '-')
  3893. .replace(/^(\d{4})\.(\d{1,2})\.(\d{1,2})(.*)$/u, '$1-$2-$3$4')
  3894. .replace(/^(\d{4}-\d{1,2}-\d{1,2})-(\d{1,2})\.(\d{1,2})\.(\d{1,2})(\.\d{1,6})?$/u, '$1 $2:$3:$4$5')
  3895. .replace(/[点时]/g, ':')
  3896. .replace(/分/g, ':')
  3897. .replace(/秒/g, '')
  3898. .replace(/\s+/g, ' ')
  3899. .trim();
  3900. return normalized.endsWith(':') ? normalized.slice(0, -1) : normalized;
  3901. };
  3902. const isValidDateOnlyValue = (value: string) => {
  3903. const normalized = normalizeDateLikeText(value);
  3904. const dashedMatch = normalized.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/u);
  3905. if (dashedMatch) {
  3906. return isValidDateParts(Number(dashedMatch[1]), Number(dashedMatch[2]), Number(dashedMatch[3]));
  3907. }
  3908. const compactMatch = normalized.match(/^(\d{4})(\d{2})(\d{2})$/u);
  3909. if (compactMatch) {
  3910. return isValidDateParts(Number(compactMatch[1]), Number(compactMatch[2]), Number(compactMatch[3]));
  3911. }
  3912. return false;
  3913. };
  3914. const isValidTimeOnlyValue = (value: string) => {
  3915. const normalized = normalizeDateLikeText(value);
  3916. const matched = normalized.match(/^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:[:.](\d{1,6}))?$/u);
  3917. if (!matched) return false;
  3918. return isValidTimeParts(Number(matched[1]), Number(matched[2]), Number(matched[3] || 0), matched[4] || '');
  3919. };
  3920. const isValidDateTimeValue = (value: string) => {
  3921. const normalized = normalizeDateLikeText(value);
  3922. if (isValidDateOnlyValue(normalized) || isValidTimeOnlyValue(normalized)) {
  3923. return true;
  3924. }
  3925. const dashedMatch = normalized.match(
  3926. /^(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:[:.](\d{1,6}))?$/u,
  3927. );
  3928. if (dashedMatch) {
  3929. return (
  3930. isValidDateParts(Number(dashedMatch[1]), Number(dashedMatch[2]), Number(dashedMatch[3])) &&
  3931. isValidTimeParts(
  3932. Number(dashedMatch[4]),
  3933. Number(dashedMatch[5]),
  3934. Number(dashedMatch[6] || 0),
  3935. dashedMatch[7] || '',
  3936. )
  3937. );
  3938. }
  3939. const compactMatch = normalized.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{1,6})?$/u);
  3940. if (compactMatch) {
  3941. return (
  3942. isValidDateParts(Number(compactMatch[1]), Number(compactMatch[2]), Number(compactMatch[3])) &&
  3943. isValidTimeParts(
  3944. Number(compactMatch[4]),
  3945. Number(compactMatch[5]),
  3946. Number(compactMatch[6]),
  3947. compactMatch[7] || '',
  3948. )
  3949. );
  3950. }
  3951. return false;
  3952. };
  3953. const isValidNumberValue = (value: string) => {
  3954. const normalized = normalizeFormatValue(value).replace(/[,\s,¥¥]/g, '');
  3955. return /^[-+]?(?:\d+\.?\d*|\.\d+)$/u.test(normalized);
  3956. };
  3957. const isValidPhoneValue = (value: string) => {
  3958. const normalized = normalizeFormatValue(value);
  3959. if (!normalized) return true;
  3960. if (/^(未知|空号|无|暂无)$/u.test(normalized)) {
  3961. return true;
  3962. }
  3963. const compactValue = normalized.replace(/[\s\-()()+]/g, '');
  3964. const digitOnlyValue = compactValue.replace(/\D/g, '');
  3965. if (digitOnlyValue.length < 5) {
  3966. return false;
  3967. }
  3968. if (/^[\d*#]+$/u.test(compactValue)) {
  3969. return true;
  3970. }
  3971. return /^(\+?\d[\d\s\-()()]{4,}|\d{5,})$/u.test(normalized);
  3972. };
  3973. const isValidAcctCardNoValue = (value: string) => {
  3974. const normalized = normalizeFormatValue(value);
  3975. if (!normalized) return true;
  3976. const compactValue = normalized.replace(/[\s\-()()]/g, '');
  3977. if (!compactValue) return true;
  3978. return /^[A-Za-z0-9*#]{6,32}$/u.test(compactValue);
  3979. };
  3980. const getIdCardCheckCode = (body: string) => {
  3981. const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
  3982. const codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
  3983. const total = body.split('').reduce((sum, char, index) => sum + Number(char) * weights[index], 0);
  3984. return codes[total % 11];
  3985. };
  3986. const isValidIdCardNumberValue = (value: string) => {
  3987. const normalized = normalizeFormatValue(value);
  3988. if (!normalized) return true;
  3989. const compactValue = normalized.replace(/\s/g, '').toUpperCase();
  3990. if (/^\d{15}$/u.test(compactValue)) {
  3991. const year = Number(`19${compactValue.slice(6, 8)}`);
  3992. const month = Number(compactValue.slice(8, 10));
  3993. const day = Number(compactValue.slice(10, 12));
  3994. return isValidDateParts(year, month, day);
  3995. }
  3996. if (!/^\d{17}[\dX]$/u.test(compactValue)) {
  3997. return false;
  3998. }
  3999. const year = Number(compactValue.slice(6, 10));
  4000. const month = Number(compactValue.slice(10, 12));
  4001. const day = Number(compactValue.slice(12, 14));
  4002. if (!isValidDateParts(year, month, day)) {
  4003. return false;
  4004. }
  4005. return getIdCardCheckCode(compactValue.slice(0, 17)) === compactValue[17];
  4006. };
  4007. const loanFlagCoreValueMap: Record<string, string> = {
  4008. 收入: '收入',
  4009. 支出: '支出',
  4010. 借方: '收入',
  4011. 贷方: '支出',
  4012. 借: '收入',
  4013. 贷: '支出',
  4014. };
  4015. const normalizeLoanFlagCoreValue = (value: string) => {
  4016. const normalized = normalizeFormatValue(value).replace(/[\s_\-()()/]/g, '');
  4017. if (!normalized) return '';
  4018. return loanFlagCoreValueMap[normalized] || '';
  4019. };
  4020. const callTypeCoreValueMap: Record<string, string> = {
  4021. 主叫: '主叫',
  4022. 被叫: '被叫',
  4023. 发送: '发送',
  4024. 接收: '接收',
  4025. 呼入: '呼入',
  4026. 呼出: '呼出',
  4027. 上行: '上行',
  4028. 下行: '下行',
  4029. 来电: '呼入',
  4030. 去电: '呼出',
  4031. CALLIN: '被叫',
  4032. CALLOUT: '主叫',
  4033. SMSIN: '接收',
  4034. SMSOUT: '发送',
  4035. };
  4036. const normalizeCallTypeCoreValue = (value: string) => {
  4037. const normalized = normalizeFormatValue(value)
  4038. .toUpperCase()
  4039. .replace(/[\s_\-()()/]/g, '');
  4040. if (!normalized) return '';
  4041. if (callTypeCoreValueMap[normalized]) {
  4042. return callTypeCoreValueMap[normalized];
  4043. }
  4044. const simplified = normalized.replace(/短信|彩信|通话|话单|记录|类型|类别|方向/g, '');
  4045. return callTypeCoreValueMap[simplified] || '';
  4046. };
  4047. const isValidLoanFlagValue = (value: string, targetKey = '') => {
  4048. const normalized = normalizeFormatValue(value);
  4049. if (!normalized) return true;
  4050. const matchedByConfig = matchRecognitionDirectionConfigValue(normalized, '借贷标志', targetKey);
  4051. if (matchedByConfig !== null) return matchedByConfig;
  4052. return !!normalizeLoanFlagCoreValue(normalized);
  4053. };
  4054. const isValidCallTypeValue = (value: string, targetKey = '') => {
  4055. const normalized = normalizeFormatValue(value);
  4056. if (!normalized) return true;
  4057. const matchedByConfig = matchRecognitionDirectionConfigValue(normalized, '通信类别', targetKey);
  4058. if (matchedByConfig !== null) return matchedByConfig;
  4059. return !!normalizeCallTypeCoreValue(normalized);
  4060. };
  4061. const isValueValidByFormatType = (value: string, formatType: FormatValidationType, targetKey = '') => {
  4062. switch (formatType) {
  4063. case 'number':
  4064. return isValidNumberValue(value);
  4065. case 'date':
  4066. return isValidDateOnlyValue(value);
  4067. case 'dateTime':
  4068. return isValidDateTimeValue(value);
  4069. case 'time':
  4070. return isValidTimeOnlyValue(value);
  4071. case 'loanFlag':
  4072. return isValidLoanFlagValue(value, targetKey);
  4073. case 'callType':
  4074. return isValidCallTypeValue(value, targetKey);
  4075. case 'phone':
  4076. return isValidPhoneValue(value);
  4077. case 'acctCardNo':
  4078. return isValidAcctCardNoValue(value);
  4079. case 'idCardNumber':
  4080. return isValidIdCardNumberValue(value);
  4081. default:
  4082. return true;
  4083. }
  4084. };
  4085. const targetFormatMetaMap = computed(() =>
  4086. fixedTargetFields.value.reduce(
  4087. (acc, field, index) => {
  4088. const templateFieldRecord = (resolveTemplateFieldMeta(field, index) || {}) as Record<string, any>;
  4089. const rawFieldType = normalizeRuleText(
  4090. templateFieldRecord.fieldType ??
  4091. templateFieldRecord.field_type ??
  4092. activeNodeTableRuleMap.value[field.key]?.fieldType,
  4093. );
  4094. acc[field.key] = {
  4095. key: field.key,
  4096. label: field.label,
  4097. formatType: normalizeFormatValidationType(rawFieldType, field.label),
  4098. };
  4099. return acc;
  4100. },
  4101. {} as Record<string, { key: string; label: string; formatType: FormatValidationType | '' }>,
  4102. ),
  4103. );
  4104. const matchedTargetFieldsByHeader = computed(() => {
  4105. return Object.entries(effectiveFieldMapping.value).reduce(
  4106. (acc, [targetKey, header]) => {
  4107. const mappedHeader = normalizeRuleText(header);
  4108. if (!mappedHeader || !acc[mappedHeader]) return acc;
  4109. const targetMeta = targetFormatMetaMap.value[targetKey];
  4110. if (targetMeta) {
  4111. acc[mappedHeader].push(targetMeta);
  4112. }
  4113. return acc;
  4114. },
  4115. rightTableHeaders.value.reduce(
  4116. (acc, header) => {
  4117. acc[header] = [];
  4118. return acc;
  4119. },
  4120. {} as Record<string, Array<{ key: string; label: string; formatType: FormatValidationType | '' }>>,
  4121. ),
  4122. );
  4123. });
  4124. const previewNormalizationTargetMetaByHeader = computed(() =>
  4125. rightTableHeaders.value.reduce(
  4126. (acc, header) => {
  4127. const matchedTarget = (matchedTargetFieldsByHeader.value[header] || []).find((item) =>
  4128. ['loanFlag', 'callType'].includes(item.formatType),
  4129. );
  4130. if (matchedTarget?.formatType) {
  4131. acc[header] = matchedTarget as {
  4132. key: string;
  4133. label: string;
  4134. formatType: 'loanFlag' | 'callType';
  4135. };
  4136. }
  4137. return acc;
  4138. },
  4139. {} as Record<string, { key: string; label: string; formatType: 'loanFlag' | 'callType' }>,
  4140. ),
  4141. );
  4142. const resolveRecognitionDirectionMatchedGroup = (
  4143. value: string,
  4144. tab: RecognitionRuleTabKey,
  4145. targetKey = '',
  4146. entries?: RecognitionDirectionItem[],
  4147. ) => {
  4148. const normalizedValue = normalizeRecognitionConfigText(value);
  4149. if (!normalizedValue) return '';
  4150. const fieldEntries =
  4151. entries ||
  4152. recognitionDirectionFields.value.find((item) => item.key === targetKey && item.tab === tab)?.entries ||
  4153. [];
  4154. const matchedEntry = fieldEntries.find((entry) => {
  4155. const comparableTexts = [entry.label, ...splitRecognitionDirectionKeywords(entry.value)];
  4156. return comparableTexts.some((keyword) => {
  4157. const normalizedKeyword = normalizeRecognitionConfigText(keyword);
  4158. return !!normalizedKeyword && normalizedKeyword === normalizedValue;
  4159. });
  4160. });
  4161. if (matchedEntry) {
  4162. return (
  4163. normalizeRecognitionDirectionGroup(matchedEntry.label, tab) ||
  4164. normalizeRecognitionConfigText(matchedEntry.label)
  4165. );
  4166. }
  4167. return '';
  4168. };
  4169. const normalizePreviewFormatValue = (
  4170. value: string,
  4171. targetMeta?: { key: string; label: string; formatType: 'loanFlag' | 'callType' },
  4172. ) => {
  4173. const normalizedValue = String(value ?? '');
  4174. if (!targetMeta || !normalizedValue) return normalizedValue;
  4175. if (targetMeta.formatType === 'loanFlag') {
  4176. return resolveRecognitionDirectionMatchedGroup(normalizedValue, '借贷标志', targetMeta.key) || normalizedValue;
  4177. }
  4178. return resolveRecognitionDirectionMatchedGroup(normalizedValue, '通信类别', targetMeta.key) || normalizedValue;
  4179. };
  4180. const normalizePreviewRowsByTargetFormat = (rows: Record<string, string>[]) =>
  4181. rows.map((row, rowIndex) => {
  4182. if (headerRowIndex.value === null || rowIndex <= headerRowIndex.value || ignoredRowsSet.value.has(rowIndex + 1)) {
  4183. return row;
  4184. }
  4185. let nextRow: Record<string, string> | null = null;
  4186. Object.entries(previewNormalizationTargetMetaByHeader.value).forEach(([header, targetMeta]) => {
  4187. const normalizedCellValue = normalizePreviewFormatValue(String(row?.[header] ?? ''), targetMeta);
  4188. if (normalizedCellValue === String(row?.[header] ?? '')) return;
  4189. if (!nextRow) {
  4190. nextRow = { ...row };
  4191. }
  4192. nextRow[header] = normalizedCellValue;
  4193. });
  4194. return nextRow || row;
  4195. });
  4196. const formatValidationRows = computed(() => {
  4197. const currentHeaderRowIndex = headerRowIndex.value;
  4198. if (currentHeaderRowIndex === null) {
  4199. return [] as Record<string, string>[];
  4200. }
  4201. return tablePreviewRows.value.filter((_row, rowIndex) => {
  4202. const lineNumber = rowIndex + 1;
  4203. if (ignoredRowsSet.value.has(lineNumber)) return false;
  4204. return rowIndex > currentHeaderRowIndex;
  4205. });
  4206. });
  4207. const buildFormatValidationTitle = (invalidTargets: HeaderFormatValidationState['invalidTargets']) =>
  4208. invalidTargets
  4209. .map(({ label, formatType, invalidValue }) => {
  4210. const shortValue = invalidValue.length > 18 ? `${invalidValue.slice(0, 18)}...` : invalidValue;
  4211. return `${label}需符合${formatTypeLabelMap[formatType]}格式,当前检测到“${shortValue}”`;
  4212. })
  4213. .join(';');
  4214. const shouldSkipFormatValidationByTarget = (targetMeta: {
  4215. key: string;
  4216. label: string;
  4217. formatType: FormatValidationType;
  4218. }) => {
  4219. const normalizedLabel = normalizeFieldMatchText(targetMeta.label);
  4220. return normalizedLabel === '号码归属省' || normalizedLabel === '号码归属市';
  4221. };
  4222. const getHeaderFormatValidationCandidates = (header: string) =>
  4223. Array.from(
  4224. (matchedTargetFieldsByHeader.value[header] || [])
  4225. .reduce((candidateMap, targetMeta) => {
  4226. if (!targetMeta.formatType) return candidateMap;
  4227. if (
  4228. shouldSkipFormatValidationByTarget(
  4229. targetMeta as {
  4230. key: string;
  4231. label: string;
  4232. formatType: FormatValidationType;
  4233. },
  4234. )
  4235. ) {
  4236. return candidateMap;
  4237. }
  4238. candidateMap.set(`${targetMeta.key}:${targetMeta.formatType}`, {
  4239. key: targetMeta.key,
  4240. label: targetMeta.label,
  4241. formatType: targetMeta.formatType,
  4242. });
  4243. return candidateMap;
  4244. }, new Map<string, { key: string; label: string; formatType: FormatValidationType }>())
  4245. .values(),
  4246. );
  4247. const headerFormatValidationMap = computed(() =>
  4248. rightTableHeaders.value.reduce(
  4249. (acc, header) => {
  4250. const invalidTargets = getHeaderFormatValidationCandidates(header).reduce(
  4251. (list, targetMeta) => {
  4252. const formatType = targetMeta.formatType;
  4253. const invalidValue =
  4254. formatValidationRows.value.find((row) => {
  4255. const cellValue = normalizeFormatValue(row[header]);
  4256. return !!cellValue && !isValueValidByFormatType(cellValue, formatType, targetMeta.key);
  4257. })?.[header] || '';
  4258. const normalizedInvalidValue = normalizeFormatValue(invalidValue);
  4259. if (!normalizedInvalidValue) return list;
  4260. list.push({
  4261. key: targetMeta.key,
  4262. label: targetMeta.label,
  4263. formatType,
  4264. invalidValue: normalizedInvalidValue,
  4265. });
  4266. return list;
  4267. },
  4268. [] as HeaderFormatValidationState['invalidTargets'],
  4269. );
  4270. acc[header] = {
  4271. invalidTargets,
  4272. title: buildFormatValidationTitle(invalidTargets),
  4273. };
  4274. return acc;
  4275. },
  4276. {} as Record<string, HeaderFormatValidationState>,
  4277. ),
  4278. );
  4279. const hasHeaderFormatError = (header: string) => !!headerFormatValidationMap.value[header]?.invalidTargets.length;
  4280. const getHeaderFormatErrorTitle = (header: string) => headerFormatValidationMap.value[header]?.title || '';
  4281. const getTargetFormatErrorState = (targetKey: string) => {
  4282. const mappedHeader = effectiveFieldMapping.value[targetKey];
  4283. if (!mappedHeader) return null;
  4284. return headerFormatValidationMap.value[mappedHeader]?.invalidTargets.find((item) => item.key === targetKey) || null;
  4285. };
  4286. const hasTargetFormatError = (targetKey: string) => !!getTargetFormatErrorState(targetKey);
  4287. const getTargetFormatErrorTitle = (targetKey: string) => {
  4288. const matchedState = getTargetFormatErrorState(targetKey);
  4289. if (!matchedState) return '';
  4290. return buildFormatValidationTitle([matchedState]);
  4291. };
  4292. const getMatchedTargetFieldText = (header: string) =>
  4293. matchedTargetFieldsByHeader.value[header]?.map((item) => item.label).join('、') || '';
  4294. const hasMatchedTargetField = (header: string) => !!matchedTargetFieldsByHeader.value[header]?.length;
  4295. const getMatchedHeaderStatusClass = (header: string) => (hasMatchedTargetField(header) ? 'is-success' : 'is-fail');
  4296. const getMatchedHeaderStatusText = (header: string) => (hasMatchedTargetField(header) ? '✓' : '✕');
  4297. const getMatchedHeaderStatusTitle = (header: string) => (hasMatchedTargetField(header) ? '已匹配' : '未匹配');
  4298. const normalizeHeaderLabel = (value: string | number | null | undefined, index: number) => {
  4299. const normalized = String(value ?? '')
  4300. .trim()
  4301. .replace(/\s+/g, ' ');
  4302. return normalized || `未命名列${index + 1}`;
  4303. };
  4304. const buildHeaderLabelsByRow = (
  4305. previousHeaders: string[],
  4306. row: Record<string, string>,
  4307. fixedHeaders: string[] = [],
  4308. ) => {
  4309. const fixedHeaderSet = new Set(fixedHeaders);
  4310. const usedLabels = new Set(fixedHeaders);
  4311. return previousHeaders.map((header, index) => {
  4312. if (fixedHeaderSet.has(header)) {
  4313. return header;
  4314. }
  4315. const baseName = normalizeHeaderLabel(row[header], index);
  4316. if (!usedLabels.has(baseName)) {
  4317. usedLabels.add(baseName);
  4318. return baseName;
  4319. }
  4320. let suffix = 2;
  4321. let candidate = `${baseName}_${suffix}`;
  4322. while (usedLabels.has(candidate)) {
  4323. suffix += 1;
  4324. candidate = `${baseName}_${suffix}`;
  4325. }
  4326. usedLabels.add(candidate);
  4327. return candidate;
  4328. });
  4329. };
  4330. const renameRecordKeys = <T,>(record: Record<string, T>, keyMap: Record<string, string>) => {
  4331. return Object.entries(record).reduce(
  4332. (acc, [key, value]) => {
  4333. acc[keyMap[key] || key] = value;
  4334. return acc;
  4335. },
  4336. {} as Record<string, T>,
  4337. );
  4338. };
  4339. const buildStoredFunPayload = <T extends object>(
  4340. payload: T,
  4341. extras?: Partial<TreeStoredFunRule>,
  4342. ): TreeStoredFunRule => ({
  4343. ...(cloneDeepSafe({ ...payload }) as TreeStoredFunRule),
  4344. ...(extras || {}),
  4345. });
  4346. const buildStoredColumnRulePayload = <T extends object>(payload: T): TreeStoredColumnRule =>
  4347. cloneDeepSafe({ ...payload }) as TreeStoredColumnRule;
  4348. const splitRegexTokens = (value: string) => {
  4349. const tokens: string[] = [];
  4350. let current = '';
  4351. let escaped = false;
  4352. for (const char of value) {
  4353. if (!escaped && char === '|') {
  4354. tokens.push(current);
  4355. current = '';
  4356. continue;
  4357. }
  4358. current += char;
  4359. escaped = !escaped && char === '\\';
  4360. if (char !== '\\') {
  4361. escaped = false;
  4362. }
  4363. }
  4364. if (current) {
  4365. tokens.push(current);
  4366. }
  4367. return tokens;
  4368. };
  4369. const parseRemoveSpecialRegex = (regex: string) => {
  4370. const normalized = String(regex || '').trim();
  4371. if (!normalized) {
  4372. return { specialList: [] as string[], specialStr: '' };
  4373. }
  4374. const body =
  4375. normalized.startsWith('(?:') && normalized.endsWith(')')
  4376. ? normalized.slice(3, normalized.length - 1)
  4377. : normalized;
  4378. const tokens = splitRegexTokens(body);
  4379. const specialList: string[] = [];
  4380. const customTokens: string[] = [];
  4381. tokens.forEach((token) => {
  4382. if (!token) return;
  4383. if (token === '\\s') {
  4384. specialList.push('移除空格字符');
  4385. return;
  4386. }
  4387. if (token === '\\r\\n') {
  4388. specialList.push('移除换行字符(tab)');
  4389. return;
  4390. }
  4391. customTokens.push(token.replace(/\\(.)/g, '$1'));
  4392. });
  4393. if (customTokens.length) {
  4394. specialList.push('要移除的特殊字符');
  4395. }
  4396. return {
  4397. specialList,
  4398. specialStr: customTokens.join('|'),
  4399. };
  4400. };
  4401. const getSplitColumnRuleIdentity = (rule?: TreeStoredColumnRule | null) => {
  4402. const refIndex = Number(rule?.refIndex);
  4403. const splitNameIndex = Number(rule?.splitNameIndex);
  4404. const splitNameIndex2 = Number(rule?.splitNameIndex2);
  4405. const len = Number(rule?.len);
  4406. const splitChar = String(rule?.splitChar ?? '');
  4407. return JSON.stringify({
  4408. refIndex: Number.isInteger(refIndex) ? refIndex : null,
  4409. mappingEnum: Number(rule?.mappingEnum ?? MappingEnum.SP_CHAR),
  4410. splitNameIndex: Number.isInteger(splitNameIndex) ? splitNameIndex : null,
  4411. splitNameIndex2: Number.isInteger(splitNameIndex2) ? splitNameIndex2 : null,
  4412. len: Number.isInteger(len) && len > 0 ? len : null,
  4413. splitChar,
  4414. });
  4415. };
  4416. const remapGroupingColumnRules = (
  4417. list: Array<Partial<GroupingColumnDraft> | Record<string, any>> | undefined,
  4418. keyMap: Record<string, string>,
  4419. removedHeaders: Set<string> = new Set(),
  4420. ) =>
  4421. normalizeGroupingColumnsDraft(list).map((item) => {
  4422. const nextHeader = removedHeaders.has(item.header) ? '' : keyMap[item.header] || item.header;
  4423. return createGroupingColumnDraft({
  4424. ...item,
  4425. header: nextHeader,
  4426. title: nextHeader || '',
  4427. });
  4428. });
  4429. const remapSelectRuleHeaderRefs = (
  4430. groups: SelectedRuleItem[][],
  4431. keyMap: Record<string, string>,
  4432. removedHeaders: Set<string> = new Set(),
  4433. ) =>
  4434. groups.map((rules) =>
  4435. rules.map((rule) => {
  4436. if (rule.key !== 'groupingColumns') {
  4437. return rule;
  4438. }
  4439. return {
  4440. ...rule,
  4441. rules: {
  4442. ...rule.rules,
  4443. groupColumns: remapGroupingColumnRules(rule.rules?.groupColumns, keyMap, removedHeaders),
  4444. },
  4445. };
  4446. }),
  4447. );
  4448. const syncHeaderRowInput = () => {
  4449. headerRowInput.value = headerRowIndex.value === null ? '' : String(headerRowIndex.value + 1);
  4450. };
  4451. const applyHeaderRow = (rowIndex: number, options: { silent?: boolean; autoMatch?: boolean } = {}) => {
  4452. const { silent = false, autoMatch = true } = options;
  4453. if (rowIndex < 0 || rowIndex >= sourceTableRows.value.length) {
  4454. syncHeaderRowInput();
  4455. if (!silent) {
  4456. createMessage.warning('请输入正确的表头行号');
  4457. }
  4458. return;
  4459. }
  4460. const previousHeaders = [...rightTableHeaders.value];
  4461. const headerSourceRow = sourceTableRows.value[rowIndex] || ({} as Record<string, string>);
  4462. const nextHeaders = buildHeaderLabelsByRow(previousHeaders, headerSourceRow, addedColumns.value);
  4463. if (
  4464. headerRowIndex.value === rowIndex &&
  4465. nextHeaders.length === previousHeaders.length &&
  4466. nextHeaders.every((header, index) => header === previousHeaders[index])
  4467. ) {
  4468. selectedRowIndex.value = rowIndex;
  4469. syncHeaderRowInput();
  4470. return;
  4471. }
  4472. const headerKeyMap = previousHeaders.reduce(
  4473. (acc, header, index) => {
  4474. acc[header] = nextHeaders[index];
  4475. return acc;
  4476. },
  4477. {} as Record<string, string>,
  4478. );
  4479. const fixedHeaderSet = new Set(addedColumns.value);
  4480. sourceTableRows.value = sourceTableRows.value.map((row, index) => {
  4481. const nextRow: Record<string, string> = {};
  4482. previousHeaders.forEach((header, colIndex) => {
  4483. const nextHeader = nextHeaders[colIndex];
  4484. if (fixedHeaderSet.has(header)) {
  4485. nextRow[nextHeader] = String(row[header] ?? '');
  4486. return;
  4487. }
  4488. nextRow[nextHeader] = index === rowIndex ? nextHeader : String(row[header] ?? '');
  4489. });
  4490. return nextRow;
  4491. });
  4492. rightTableHeaders.value = nextHeaders;
  4493. generatedColumnHeaderRowMap.value = addedColumns.value.reduce(
  4494. (acc, item) => {
  4495. const nextKey = headerKeyMap[item] || item;
  4496. acc[nextKey] = getGeneratedColumnHeaderRowIndex(item);
  4497. return acc;
  4498. },
  4499. {} as Record<string, number | null>,
  4500. );
  4501. addedColumns.value = addedColumns.value.map((item) => headerKeyMap[item] || item);
  4502. selectedAddedColumn.value = selectedAddedColumn.value ? headerKeyMap[selectedAddedColumn.value] || '' : '';
  4503. fieldMapping.value = Object.entries(fieldMapping.value).reduce(
  4504. (acc, [key, value]) => {
  4505. acc[key] = headerKeyMap[value] || value;
  4506. return acc;
  4507. },
  4508. {} as Record<string, string>,
  4509. );
  4510. addedColumnConfig.value = Object.entries(renameRecordKeys(addedColumnConfig.value, headerKeyMap)).reduce(
  4511. (acc, [key, value]) => {
  4512. acc[key] = {
  4513. ...value,
  4514. contentFields: value.contentFields.map((field) => headerKeyMap[field] || field),
  4515. };
  4516. return acc;
  4517. },
  4518. {} as Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>,
  4519. );
  4520. splitColumnConfig.value = Object.entries(renameRecordKeys(splitColumnConfig.value, headerKeyMap)).reduce(
  4521. (acc, [key, value]) => {
  4522. acc[key] = {
  4523. ...value,
  4524. source: headerKeyMap[value.source] || value.source,
  4525. targetName1: headerKeyMap[value.targetName1] || value.targetName1,
  4526. targetName2: headerKeyMap[value.targetName2] || value.targetName2,
  4527. };
  4528. return acc;
  4529. },
  4530. {} as Record<
  4531. string,
  4532. { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
  4533. >,
  4534. );
  4535. selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, headerKeyMap);
  4536. groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, headerKeyMap);
  4537. if (splitColumnForm.value.source) {
  4538. splitColumnForm.value.source = headerKeyMap[splitColumnForm.value.source] || splitColumnForm.value.source;
  4539. }
  4540. headerRowIndex.value = rowIndex;
  4541. selectedRowIndex.value = rowIndex;
  4542. syncHeaderRowInput();
  4543. rebuildGeneratedColumnsInSourceRows();
  4544. runRulePipeline();
  4545. if (autoMatch) {
  4546. autoMatchFields(false);
  4547. }
  4548. if (!silent) {
  4549. createMessage.success(`已将第${rowIndex + 1}行设为表头行`);
  4550. }
  4551. };
  4552. const applyHeaderRowInput = () => {
  4553. const rawValue = headerRowInput.value.trim();
  4554. if (!rawValue) {
  4555. syncHeaderRowInput();
  4556. return;
  4557. }
  4558. const rowNumber = Number(rawValue);
  4559. if (!Number.isInteger(rowNumber)) {
  4560. syncHeaderRowInput();
  4561. createMessage.warning('请输入正确的表头行号');
  4562. return;
  4563. }
  4564. applyHeaderRow(rowNumber - 1);
  4565. };
  4566. const restorePreviewFromNode = async (item?: TreeFileInfo | null) => {
  4567. const previewDataset = await loadPreviewDatasetFromFile(item);
  4568. const hasPresetHeaders = previewDataset ? applyPreviewDataset(previewDataset) : applyPresetHeadersFromNode(item);
  4569. const headerRow = normalizeStoredLineNoToRowIndex(item?.lineNo);
  4570. if (headerRow !== null && !hasPresetHeaders) {
  4571. applyHeaderRow(headerRow, { silent: true, autoMatch: false });
  4572. return;
  4573. }
  4574. headerRowIndex.value = headerRow;
  4575. selectedRowIndex.value = headerRow;
  4576. syncHeaderRowInput();
  4577. };
  4578. const ensureCurrentHeaderRowConfigured = (warningText = '请先填写表头所在行,再进行模板匹配') => {
  4579. if (headerRowIndex.value !== null) return true;
  4580. const rawValue = headerRowInput.value.trim();
  4581. if (!rawValue) {
  4582. createMessage.warning(warningText);
  4583. return false;
  4584. }
  4585. const rowNumber = Number(rawValue);
  4586. if (!Number.isInteger(rowNumber) || rowNumber <= 0 || rowNumber > sourceTableRows.value.length) {
  4587. createMessage.warning('请输入正确的表头行号');
  4588. return false;
  4589. }
  4590. applyHeaderRow(rowNumber - 1, { silent: true, autoMatch: false });
  4591. return headerRowIndex.value !== null;
  4592. };
  4593. const hasNodeTemplateMatched = (item?: TreeFileInfo | null) => {
  4594. if (!isTreeSheetNode(item)) return false;
  4595. return (
  4596. normalizeTemplateId(item?.templateId) > 0 ||
  4597. normalizeTableRuleList(item?.tableRuleList).length > 0 ||
  4598. !!normalizeRuleText(item?.tableName)
  4599. );
  4600. };
  4601. const collectMatchedSheetsWithoutHeaderRow = (items?: TreeFileInfo[] | null) => {
  4602. const missingSheets: string[] = [];
  4603. const walk = (nodes?: TreeFileInfo[] | null) => {
  4604. if (!Array.isArray(nodes) || !nodes.length) return;
  4605. nodes.forEach((node) => {
  4606. if (hasNodeTemplateMatched(node) && normalizeStoredLineNoToRowIndex(node?.lineNo) === null) {
  4607. missingSheets.push(getTreeSheetName(node));
  4608. }
  4609. if (Array.isArray(node.children) && node.children.length) {
  4610. walk(node.children);
  4611. }
  4612. });
  4613. };
  4614. walk(items);
  4615. return missingSheets;
  4616. };
  4617. const mockIndexColumnWidth = 72;
  4618. const mockDataColumnWidth = 168;
  4619. const mockGridTemplate = computed(() =>
  4620. [`${mockIndexColumnWidth}px`, ...rightTableHeaders.value.map(() => `${mockDataColumnWidth}px`)].join(' '),
  4621. );
  4622. const getColumnRuleBadgeState = (colIndex: number) => {
  4623. if (colIndex < 0) return false;
  4624. const rules = sanitizeSelectedRuleList(selectRules.value[colIndex] || []);
  4625. const hasEnabledRules = rules.some((rule) => rule && rule.key && rule.enabled);
  4626. if (hasEnabledRules) return 'active';
  4627. return rules.some((rule) => rule && rule.key) ? 'draft' : '';
  4628. };
  4629. const getColumnRuleBadgeTitle = (colIndex: number) => {
  4630. const state = getColumnRuleBadgeState(colIndex);
  4631. if (state === 'active') return '该列已启用清洗流程';
  4632. if (state === 'draft') return '该列已添加清洗规则,待启用';
  4633. return '';
  4634. };
  4635. const addColumnDialogOpen = ref(false);
  4636. const splitColumnDialogOpen = ref(false);
  4637. const deleteColumnDialogOpen = ref(false);
  4638. const addColumnMode = ref<'add' | 'edit'>('add');
  4639. const addColumnForm = ref<{
  4640. name: string;
  4641. mode: 'ref' | 'custom';
  4642. contentFields: string[];
  4643. customContent: string;
  4644. }>({
  4645. name: '',
  4646. mode: 'ref',
  4647. contentFields: [],
  4648. customContent: '',
  4649. });
  4650. const addedColumnConfig = ref<
  4651. Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>
  4652. >({});
  4653. const splitColumnConfig = ref<
  4654. Record<
  4655. string,
  4656. { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
  4657. >
  4658. >({});
  4659. const selectedSourceField = ref('');
  4660. const selectedContentIndex = ref(-1);
  4661. const draggingContentIndex = ref<number | null>(null);
  4662. const dragOverContentIndex = ref<number | null>(null);
  4663. const availableSourceHeaders = computed(() =>
  4664. rightTableHeaders.value.filter((item) => item && !addedColumns.value.includes(item)),
  4665. );
  4666. const sourceFieldCandidates = computed(() => availableSourceHeaders.value);
  4667. const normalizeColumnNameForCompare = (value: unknown) =>
  4668. String(value ?? '')
  4669. .trim()
  4670. .toLocaleLowerCase();
  4671. const hasDuplicateHeaderName = (name: string, excludedRawNames: string[] = []) => {
  4672. const normalizedName = normalizeColumnNameForCompare(name);
  4673. if (!normalizedName) return false;
  4674. return rightTableHeaders.value.some(
  4675. (header) => !excludedRawNames.includes(header) && normalizeColumnNameForCompare(header) === normalizedName,
  4676. );
  4677. };
  4678. const normalizeSplitColumnMode = (value?: unknown) => {
  4679. const rawMode = normalizeRuleText(value);
  4680. if (rawMode === 'length' || rawMode === 'special') {
  4681. return rawMode as 'length' | 'special';
  4682. }
  4683. if (rawMode === 'comma' || rawMode === 'space' || rawMode === 'custom') {
  4684. return 'special' as const;
  4685. }
  4686. return 'length' as const;
  4687. };
  4688. const normalizeSplitColumnValue = (type?: unknown, value?: unknown) => {
  4689. const rawType = normalizeRuleText(type);
  4690. if (rawType === 'comma') return ',';
  4691. if (rawType === 'space') return ' ';
  4692. return normalizeRuleText(value);
  4693. };
  4694. const resolveSplitColumnLength = (type?: unknown, value?: unknown) => {
  4695. if (normalizeSplitColumnMode(type) !== 'length') return 0;
  4696. const length = Number(normalizeSplitColumnValue(type, value));
  4697. return Number.isInteger(length) && length > 0 ? length : 0;
  4698. };
  4699. const splitSourceValueByConfig = (sourceValue: string, type?: unknown, value?: unknown) => {
  4700. const splitMode = normalizeSplitColumnMode(type);
  4701. const normalizedValue = normalizeSplitColumnValue(type, value);
  4702. if (!sourceValue) {
  4703. return { first: '', second: '' };
  4704. }
  4705. if (splitMode === 'length') {
  4706. const splitLength = resolveSplitColumnLength(type, value);
  4707. if (!splitLength) {
  4708. return { first: sourceValue, second: '' };
  4709. }
  4710. return {
  4711. first: sourceValue.slice(0, splitLength),
  4712. second: sourceValue.slice(splitLength),
  4713. };
  4714. }
  4715. if (!normalizedValue) {
  4716. return { first: sourceValue, second: '' };
  4717. }
  4718. const splitIndex = sourceValue.indexOf(normalizedValue);
  4719. if (splitIndex < 0) {
  4720. return { first: sourceValue, second: '' };
  4721. }
  4722. return {
  4723. first: sourceValue.slice(0, splitIndex),
  4724. second: sourceValue.slice(splitIndex + normalizedValue.length),
  4725. };
  4726. };
  4727. const splitColumnForm = ref({
  4728. source: '',
  4729. targetName1: '',
  4730. targetName2: '',
  4731. delimiterType: 'length',
  4732. delimiterValue: '',
  4733. });
  4734. const delimiterOptions = [
  4735. { label: '按字符长度', value: 'length' },
  4736. { label: '按特殊标识', value: 'special' },
  4737. ];
  4738. const columnTypeOptions = [
  4739. { label: '字符串', value: 'string' },
  4740. { label: '数字', value: 'number' },
  4741. { label: '日期时间', value: 'datetime' },
  4742. ];
  4743. const splitSourceOptions = computed(() =>
  4744. availableSourceHeaders.value.map((item) => ({
  4745. label: item,
  4746. value: item,
  4747. })),
  4748. );
  4749. const originalFieldOptions = computed(() =>
  4750. rightTableHeaders.value.map((item) => ({
  4751. label: item,
  4752. value: item,
  4753. })),
  4754. );
  4755. const fieldMapping = ref<Record<string, string>>({});
  4756. const mappingPopoverOpenKey = ref<string>('');
  4757. const originFilterKeyword = ref('');
  4758. const hoveredOriginOptionTargetKey = ref('');
  4759. const hoveredOriginOptionValue = ref('');
  4760. const originHoverPreviewSnapshot = ref<PreviewColumnSnapshot | null>(null);
  4761. const recognitionAmountZfEnabled = ref(false);
  4762. const resolveTargetKeyByTableRule = (rule: TreeTableRuleInfo) => {
  4763. const fieldNameEn = normalizeRuleText(rule.fieldNameEn);
  4764. if (fieldNameEn) {
  4765. const matchedByKey = fixedTargetFields.value.find((item) => item.key === fieldNameEn);
  4766. if (matchedByKey) return matchedByKey.key;
  4767. }
  4768. const fieldNameCn = normalizeRuleText(rule.fieldNameCn);
  4769. if (fieldNameCn) {
  4770. const matchedByLabel =
  4771. fixedTargetFields.value.find((item) => item.label === fieldNameCn) ||
  4772. fixedTargetFields.value.find((item) => item.label.includes(fieldNameCn) || fieldNameCn.includes(item.label));
  4773. if (matchedByLabel) return matchedByLabel.key;
  4774. }
  4775. return '';
  4776. };
  4777. const applyTableRuleMapping = (list?: TreeTableRuleInfo[] | null) => {
  4778. const tableRules = normalizeTableRuleList(list);
  4779. if (!tableRules.length || !fixedTargetFields.value.length) return false;
  4780. const headerSet = new Set(rightTableHeaders.value);
  4781. const nextMapping = tableRules.reduce(
  4782. (acc, item) => {
  4783. const targetKey = resolveTargetKeyByTableRule(item);
  4784. const sourceField = normalizeRuleText(item.fileColName);
  4785. if (!targetKey || !sourceField || !headerSet.has(sourceField)) return acc;
  4786. acc[targetKey] = sourceField;
  4787. return acc;
  4788. },
  4789. {} as Record<string, string>,
  4790. );
  4791. if (!Object.keys(nextMapping).length) return false;
  4792. fieldMapping.value = { ...fieldMapping.value, ...nextMapping };
  4793. return true;
  4794. };
  4795. const filteredOriginalOptions = computed(() => {
  4796. if (!originFilterKeyword.value) return originalFieldOptions.value;
  4797. return originalFieldOptions.value.filter((item) => item.label.includes(originFilterKeyword.value));
  4798. });
  4799. const resolveRecognitionBasicInfoTargetKey = (
  4800. targetField: unknown,
  4801. fields: MappingTargetField[] = fixedTargetFields.value,
  4802. ) => {
  4803. const rawTargetField = normalizeRecognitionConfigText(targetField);
  4804. if (!rawTargetField || !fields.length) return '';
  4805. const matchedByKey = fields.find((item) => item.key === rawTargetField);
  4806. if (matchedByKey) return matchedByKey.key;
  4807. const matchedByLabel = fields.find((item) => item.label === rawTargetField);
  4808. if (matchedByLabel) return matchedByLabel.key;
  4809. const normalizedTargetField = normalizeFieldMatchText(rawTargetField);
  4810. if (!normalizedTargetField) return '';
  4811. const matchedByNormalized = fields.find(
  4812. (item) =>
  4813. normalizeFieldMatchText(item.key) === normalizedTargetField ||
  4814. normalizeFieldMatchText(item.label) === normalizedTargetField,
  4815. );
  4816. if (matchedByNormalized) return matchedByNormalized.key;
  4817. const matchedByFuzzy = fields.find((item) => {
  4818. const normalizedKey = normalizeFieldMatchText(item.key);
  4819. const normalizedLabel = normalizeFieldMatchText(item.label);
  4820. return (
  4821. (!!normalizedKey &&
  4822. (normalizedKey.includes(normalizedTargetField) || normalizedTargetField.includes(normalizedKey))) ||
  4823. (!!normalizedLabel &&
  4824. (normalizedLabel.includes(normalizedTargetField) || normalizedTargetField.includes(normalizedLabel)))
  4825. );
  4826. });
  4827. return matchedByFuzzy?.key || '';
  4828. };
  4829. const buildRecognitionBasicInfoMatchedTargetKeySet = (
  4830. rows: RecognitionBasicInfoRow[] = [],
  4831. fields: MappingTargetField[] = fixedTargetFields.value,
  4832. ) =>
  4833. rows.reduce((acc, item) => {
  4834. if (!normalizeRecognitionConfigText(item.headerMatch)) return acc;
  4835. const targetKey = resolveRecognitionBasicInfoTargetKey(item.targetField, fields);
  4836. if (targetKey) {
  4837. acc.add(targetKey);
  4838. }
  4839. return acc;
  4840. }, new Set<string>());
  4841. const buildEffectiveFieldMapping = (mapping: Record<string, string>, lockedTargetKeySet: Set<string>) =>
  4842. Object.entries(mapping).reduce(
  4843. (acc, [targetKey, header]) => {
  4844. const mappedHeader = normalizeRuleText(header);
  4845. if (!mappedHeader || lockedTargetKeySet.has(targetKey)) return acc;
  4846. acc[targetKey] = mappedHeader;
  4847. return acc;
  4848. },
  4849. {} as Record<string, string>,
  4850. );
  4851. const recognitionBasicInfoMatchedTargetKeySet = computed(() =>
  4852. buildRecognitionBasicInfoMatchedTargetKeySet(recognitionBasicInfoRows.value, fixedTargetFields.value),
  4853. );
  4854. const isTargetMatchedByRecognitionBasicInfo = (targetKey: string) =>
  4855. recognitionBasicInfoMatchedTargetKeySet.value.has(targetKey);
  4856. const getTargetFieldByKey = (targetKey: string, fields: MappingTargetField[] = fixedTargetFields.value) =>
  4857. fields.find((item) => item.key === targetKey) || null;
  4858. const buildAmountZfHandledTargetKeySet = (
  4859. fields: MappingTargetField[] = fixedTargetFields.value,
  4860. enabled = recognitionAmountZfEnabled.value,
  4861. ) =>
  4862. fields.reduce((acc, field) => {
  4863. if (enabled && isLoanFlagTargetField(field)) {
  4864. acc.add(field.key);
  4865. }
  4866. return acc;
  4867. }, new Set<string>());
  4868. const isTargetHandledByAmountZf = (targetKey: string) => {
  4869. const field = getTargetFieldByKey(targetKey);
  4870. return !!field && recognitionAmountZfEnabled.value && isLoanFlagTargetField(field);
  4871. };
  4872. const recognitionRuleHandledTargetKeySet = computed(() => {
  4873. const nextSet = new Set(recognitionBasicInfoMatchedTargetKeySet.value);
  4874. buildAmountZfHandledTargetKeySet(fixedTargetFields.value, recognitionAmountZfEnabled.value).forEach((key) => {
  4875. nextSet.add(key);
  4876. });
  4877. return nextSet;
  4878. });
  4879. const isTargetHandledByRecognitionRule = (targetKey: string) =>
  4880. recognitionRuleHandledTargetKeySet.value.has(targetKey);
  4881. const effectiveFieldMapping = computed(() =>
  4882. buildEffectiveFieldMapping(fieldMapping.value, recognitionRuleHandledTargetKeySet.value),
  4883. );
  4884. const getDisplayedMappedHeader = (targetKey: string) =>
  4885. isTargetHandledByRecognitionRule(targetKey) ? '' : fieldMapping.value[targetKey] || '';
  4886. const isTargetMatched = (targetKey: string) =>
  4887. isTargetHandledByRecognitionRule(targetKey) || !!effectiveFieldMapping.value[targetKey];
  4888. const resolveMappedTargetKeyByHeader = (header: string) => {
  4889. const normalizedHeader = normalizeRuleText(header);
  4890. if (!normalizedHeader) return '';
  4891. return (
  4892. fixedTargetFields.value.find((item) => normalizeRuleText(getDisplayedMappedHeader(item.key)) === normalizedHeader)
  4893. ?.key || ''
  4894. );
  4895. };
  4896. const getPreviewColumnTarget = (colIndex: number) => {
  4897. const container = tableMockRef.value;
  4898. if (!container) return null;
  4899. return (
  4900. container.querySelector<HTMLElement>(`.mock-row > span[data-preview-col="${colIndex}"]`) ||
  4901. container.querySelector<HTMLElement>(`.mock-match-row > span[data-preview-col="${colIndex}"]`) ||
  4902. container.querySelector<HTMLElement>(`.mock-head > span[data-preview-col="${colIndex}"]`) ||
  4903. null
  4904. );
  4905. };
  4906. const isVisibleInDom = (element: HTMLElement) => {
  4907. return element.offsetParent !== null || element.getClientRects().length > 0;
  4908. };
  4909. const getVisibleOriginOptionList = (targetKey: string) => {
  4910. const lists = Array.from(
  4911. document.querySelectorAll<HTMLElement>(
  4912. `.origin-field-popover .origin-pop-list[data-origin-list-target-key="${targetKey}"]`,
  4913. ),
  4914. );
  4915. return lists.find((item) => isVisibleInDom(item)) || lists[0] || null;
  4916. };
  4917. const scrollOriginOptionIntoView = (targetKey: string) => {
  4918. const optionValue = normalizeRuleText(getDisplayedMappedHeader(targetKey));
  4919. if (!optionValue) return;
  4920. void nextTick(() => {
  4921. const tryScroll = (attempt = 0) => {
  4922. const list = getVisibleOriginOptionList(targetKey);
  4923. if (!list) {
  4924. if (attempt < 6) {
  4925. window.requestAnimationFrame(() => tryScroll(attempt + 1));
  4926. }
  4927. return;
  4928. }
  4929. const options = Array.from(
  4930. list.querySelectorAll<HTMLElement>(`[data-origin-target-key="${targetKey}"][data-origin-option-value]`),
  4931. );
  4932. const option =
  4933. options.find((item) => normalizeRuleText(item.dataset.originOptionValue) === optionValue) || null;
  4934. const scrollTarget = option?.closest<HTMLElement>('.origin-tag-wrap') || option;
  4935. scrollTarget?.scrollIntoView({
  4936. block: 'nearest',
  4937. inline: 'nearest',
  4938. behavior: 'smooth',
  4939. });
  4940. };
  4941. window.requestAnimationFrame(() => {
  4942. window.requestAnimationFrame(() => {
  4943. tryScroll();
  4944. });
  4945. });
  4946. });
  4947. };
  4948. const scrollPreviewColumnIntoView = (colIndex: number) => {
  4949. if (colIndex < 0) return;
  4950. void nextTick(() => {
  4951. window.requestAnimationFrame(() => {
  4952. window.requestAnimationFrame(() => {
  4953. const container = tableMockRef.value;
  4954. const target = getPreviewColumnTarget(colIndex);
  4955. if (!container || !target) return;
  4956. const containerRect = container.getBoundingClientRect();
  4957. const targetRect = target.getBoundingClientRect();
  4958. const targetLeft = container.scrollLeft + (targetRect.left - containerRect.left);
  4959. const desiredLeft = targetLeft - mockIndexColumnWidth - 16;
  4960. const maxScrollLeft = Math.max(container.scrollWidth - container.clientWidth, 0);
  4961. container.scrollTo({
  4962. left: Math.min(Math.max(desiredLeft, 0), maxScrollLeft),
  4963. behavior: 'smooth',
  4964. });
  4965. });
  4966. });
  4967. });
  4968. };
  4969. const applyPreviewColumnState = (
  4970. header: string,
  4971. colIndex: number,
  4972. options: {
  4973. resetSelectedRow?: boolean;
  4974. activeTargetKey?: string;
  4975. } = {},
  4976. ) => {
  4977. if (colIndex < 0) return;
  4978. currentColumn.value = colIndex;
  4979. if (options.resetSelectedRow ?? true) {
  4980. selectedRowIndex.value = null;
  4981. }
  4982. selectedAddedColumn.value = addedColumns.value.includes(header) ? header : '';
  4983. activeMappingTargetKey.value =
  4984. options.activeTargetKey !== undefined ? options.activeTargetKey : resolveMappedTargetKeyByHeader(header);
  4985. scrollPreviewColumnIntoView(colIndex);
  4986. };
  4987. const capturePreviewColumnSnapshot = (): PreviewColumnSnapshot => ({
  4988. colIndex: currentColumn.value,
  4989. selectedRowIndex: selectedRowIndex.value,
  4990. selectedAddedColumn: selectedAddedColumn.value,
  4991. activeTargetKey: activeMappingTargetKey.value,
  4992. });
  4993. const clearOriginHoverPreviewState = () => {
  4994. hoveredOriginOptionTargetKey.value = '';
  4995. hoveredOriginOptionValue.value = '';
  4996. originHoverPreviewSnapshot.value = null;
  4997. };
  4998. const restoreOriginHoverPreview = (targetKey?: string) => {
  4999. if (targetKey && hoveredOriginOptionTargetKey.value && hoveredOriginOptionTargetKey.value !== targetKey) {
  5000. return;
  5001. }
  5002. const snapshot = originHoverPreviewSnapshot.value;
  5003. clearOriginHoverPreviewState();
  5004. if (!snapshot) return;
  5005. currentColumn.value = snapshot.colIndex;
  5006. selectedRowIndex.value = snapshot.selectedRowIndex;
  5007. selectedAddedColumn.value = snapshot.selectedAddedColumn;
  5008. activeMappingTargetKey.value = snapshot.activeTargetKey;
  5009. if (snapshot.colIndex !== null && snapshot.colIndex >= 0) {
  5010. scrollPreviewColumnIntoView(snapshot.colIndex);
  5011. }
  5012. };
  5013. const focusPreviewColumn = (
  5014. header: string,
  5015. colIndex: number,
  5016. options: {
  5017. resetSelectedRow?: boolean;
  5018. activeTargetKey?: string;
  5019. } = {},
  5020. ) => {
  5021. applyPreviewColumnState(header, colIndex, options);
  5022. };
  5023. const syncMappingSelectionState = (targetKey: string) => {
  5024. activeMappingTargetKey.value = targetKey;
  5025. const mappedHeader = normalizeRuleText(getDisplayedMappedHeader(targetKey));
  5026. if (mappedHeader) {
  5027. const colIndex = rightTableHeaders.value.findIndex((header) => normalizeRuleText(header) === mappedHeader);
  5028. if (colIndex >= 0) {
  5029. focusPreviewColumn(rightTableHeaders.value[colIndex], colIndex, {
  5030. resetSelectedRow: true,
  5031. activeTargetKey: targetKey,
  5032. });
  5033. return;
  5034. }
  5035. }
  5036. const targetLabel = normalizeFieldMatchText(
  5037. fixedTargetFields.value.find((item) => item.key === targetKey)?.label || '',
  5038. );
  5039. if (!targetLabel) return;
  5040. const fallbackColIndex = rightTableHeaders.value.findIndex(
  5041. (header) => normalizeFieldMatchText(header) === targetLabel,
  5042. );
  5043. if (fallbackColIndex < 0) return;
  5044. focusPreviewColumn(rightTableHeaders.value[fallbackColIndex], fallbackColIndex, {
  5045. resetSelectedRow: true,
  5046. activeTargetKey: targetKey,
  5047. });
  5048. };
  5049. const handleMappingRowSelect = (targetKey: string) => {
  5050. syncMappingSelectionState(targetKey);
  5051. if (!isTargetHandledByRecognitionRule(targetKey)) {
  5052. mappingPopoverOpenKey.value = targetKey;
  5053. originFilterKeyword.value = '';
  5054. scrollOriginOptionIntoView(targetKey);
  5055. }
  5056. };
  5057. const handleOriginOptionHover = (targetKey: string, fieldValue: string) => {
  5058. const normalizedFieldValue = normalizeRuleText(fieldValue);
  5059. if (!normalizedFieldValue) return;
  5060. if (!originHoverPreviewSnapshot.value || hoveredOriginOptionTargetKey.value !== targetKey) {
  5061. originHoverPreviewSnapshot.value = capturePreviewColumnSnapshot();
  5062. }
  5063. hoveredOriginOptionTargetKey.value = targetKey;
  5064. hoveredOriginOptionValue.value = fieldValue;
  5065. const colIndex = rightTableHeaders.value.findIndex((header) => normalizeRuleText(header) === normalizedFieldValue);
  5066. if (colIndex < 0) return;
  5067. applyPreviewColumnState(rightTableHeaders.value[colIndex], colIndex, {
  5068. resetSelectedRow: true,
  5069. activeTargetKey: targetKey,
  5070. });
  5071. };
  5072. const handleOriginOptionHoverLeave = (targetKey: string) => {
  5073. restoreOriginHoverPreview(targetKey);
  5074. };
  5075. const isOriginOptionHovering = (targetKey: string, fieldValue: string) => {
  5076. return (
  5077. hoveredOriginOptionTargetKey.value === targetKey &&
  5078. normalizeRuleText(hoveredOriginOptionValue.value) === normalizeRuleText(fieldValue)
  5079. );
  5080. };
  5081. const isFieldTakenByOther = (fieldValue: string, currentKey: string) => {
  5082. return Object.entries(effectiveFieldMapping.value).some(
  5083. ([key, value]) => key !== currentKey && value === fieldValue,
  5084. );
  5085. };
  5086. const handleMappingPopoverOpen = (key: string, open: boolean) => {
  5087. if (isTargetHandledByRecognitionRule(key)) {
  5088. mappingPopoverOpenKey.value = '';
  5089. originFilterKeyword.value = '';
  5090. restoreOriginHoverPreview(key);
  5091. return;
  5092. }
  5093. mappingPopoverOpenKey.value = open ? key : '';
  5094. if (open) {
  5095. handleMappingRowSelect(key);
  5096. }
  5097. if (!open) {
  5098. restoreOriginHoverPreview(key);
  5099. originFilterKeyword.value = '';
  5100. }
  5101. };
  5102. const handleMappingTriggerClick = (targetKey: string) => {
  5103. if (isTargetHandledByRecognitionRule(targetKey)) {
  5104. activeMappingTargetKey.value = targetKey;
  5105. mappingPopoverOpenKey.value = '';
  5106. originFilterKeyword.value = '';
  5107. return;
  5108. }
  5109. handleMappingRowSelect(targetKey);
  5110. mappingPopoverOpenKey.value = targetKey;
  5111. };
  5112. const closeMappingPopoverPreview = () => {
  5113. mappingPopoverOpenKey.value = '';
  5114. originFilterKeyword.value = '';
  5115. clearOriginHoverPreviewState();
  5116. };
  5117. const setFieldBlank = (targetKey: string) => {
  5118. if (isTargetHandledByRecognitionRule(targetKey)) return;
  5119. restoreOriginHoverPreview(targetKey);
  5120. activeMappingTargetKey.value = targetKey;
  5121. fieldMapping.value[targetKey] = '';
  5122. refreshRecognitionDirectionState(activeNodeInfo.value);
  5123. mappingPopoverOpenKey.value = '';
  5124. };
  5125. const selectOriginalField = (targetKey: string, fieldValue: string) => {
  5126. if (isTargetHandledByRecognitionRule(targetKey)) return;
  5127. if (isFieldTakenByOther(fieldValue, targetKey)) return;
  5128. if (normalizeRuleText(getDisplayedMappedHeader(targetKey)) === normalizeRuleText(fieldValue)) {
  5129. setFieldBlank(targetKey);
  5130. return;
  5131. }
  5132. clearOriginHoverPreviewState();
  5133. fieldMapping.value[targetKey] = fieldValue;
  5134. refreshRecognitionDirectionState(activeNodeInfo.value);
  5135. mappingPopoverOpenKey.value = '';
  5136. syncMappingSelectionState(targetKey);
  5137. };
  5138. const replaceOriginalField = (targetKey: string, fieldValue: string) => {
  5139. if (isTargetHandledByRecognitionRule(targetKey)) return;
  5140. clearOriginHoverPreviewState();
  5141. Object.keys(effectiveFieldMapping.value).forEach((key) => {
  5142. if (key !== targetKey && effectiveFieldMapping.value[key] === fieldValue) {
  5143. fieldMapping.value[key] = '';
  5144. }
  5145. });
  5146. fieldMapping.value[targetKey] = fieldValue;
  5147. refreshRecognitionDirectionState(activeNodeInfo.value);
  5148. mappingPopoverOpenKey.value = '';
  5149. syncMappingSelectionState(targetKey);
  5150. };
  5151. const childTemplateName = ref('');
  5152. const showChildTemplateExistsWarning = (name: string) => {
  5153. Modal.warning({
  5154. title: '子模板名称已存在',
  5155. content: `子模板名称“${name}”已存在,请修改后再导入数据。`,
  5156. centered: true,
  5157. });
  5158. };
  5159. const showDuplicateChildTemplateWarning = (names: string[]) => {
  5160. const previewNames = names.slice(0, 3).join('、');
  5161. const suffix = names.length > 3 ? ` 等 ${names.length} 个名称` : '';
  5162. Modal.warning({
  5163. title: '子模板名称重复',
  5164. content: `当前清洗结果中存在重复的子模板名称:${previewNames}${suffix},请修改后再导入数据。`,
  5165. centered: true,
  5166. });
  5167. };
  5168. const checkTemplateNameAvailable = async (tableNameCn: string) => {
  5169. const response = await tplCheckTableName('', tableNameCn);
  5170. // const available = resolveTemplateNameCheckResult(response);
  5171. // if (available === null) {
  5172. // throw new Error('模板名称校验结果无法识别');
  5173. // }
  5174. return response === 'N';
  5175. };
  5176. const handleChildTemplateNameBlur = async () => {
  5177. const tableNameCn = normalizeRuleText(childTemplateName.value);
  5178. if (!tableNameCn) return;
  5179. try {
  5180. const available = await checkTemplateNameAvailable(tableNameCn);
  5181. if (!available) {
  5182. showChildTemplateExistsWarning(tableNameCn);
  5183. }
  5184. } catch (error) {
  5185. createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
  5186. }
  5187. };
  5188. const collectChildTemplateNames = (nodes: TreeFileInfo[] = []) => {
  5189. const names: string[] = [];
  5190. const walk = (items: TreeFileInfo[] = []) => {
  5191. items.forEach((item) => {
  5192. const childName = normalizeRuleText(item.childTemplateName);
  5193. if (childName) {
  5194. names.push(childName);
  5195. }
  5196. if (Array.isArray(item.children) && item.children.length) {
  5197. walk(item.children);
  5198. }
  5199. });
  5200. };
  5201. walk(nodes);
  5202. return names;
  5203. };
  5204. const findDuplicateChildTemplateNames = (names: string[]) => {
  5205. const duplicateNameMap = new Map<string, string>();
  5206. const counter = new Map<string, number>();
  5207. names.forEach((name) => {
  5208. const key = normalizeTemplateNameKey(name);
  5209. if (!key) return;
  5210. const nextCount = (counter.get(key) || 0) + 1;
  5211. counter.set(key, nextCount);
  5212. if (nextCount > 1 && !duplicateNameMap.has(key)) {
  5213. duplicateNameMap.set(key, name);
  5214. }
  5215. });
  5216. return Array.from(duplicateNameMap.values());
  5217. };
  5218. const ensureChildTemplateNamesAvailable = async (nodes: TreeFileInfo[] = []) => {
  5219. const childTemplateNames = collectChildTemplateNames(nodes);
  5220. if (!childTemplateNames.length) return true;
  5221. const duplicateNames = findDuplicateChildTemplateNames(childTemplateNames);
  5222. if (duplicateNames.length) {
  5223. showDuplicateChildTemplateWarning(duplicateNames);
  5224. return false;
  5225. }
  5226. const uniqueNameMap = new Map<string, string>();
  5227. childTemplateNames.forEach((name) => {
  5228. const key = normalizeTemplateNameKey(name);
  5229. if (key && !uniqueNameMap.has(key)) {
  5230. uniqueNameMap.set(key, name);
  5231. }
  5232. });
  5233. for (const name of uniqueNameMap.values()) {
  5234. const available = await checkTemplateNameAvailable(name);
  5235. if (!available) {
  5236. showChildTemplateExistsWarning(name);
  5237. return false;
  5238. }
  5239. }
  5240. return true;
  5241. };
  5242. const cleaningRulePanelExpanded = ref(false);
  5243. const recognitionPanelExpanded = ref(false);
  5244. const activeRecognitionRule = ref('');
  5245. const recognitionDirectionFields = ref<RecognitionDirectionField[]>([]);
  5246. const recognitionBasicInfoBaseFuncRegx = ref('');
  5247. const recognitionBasicInfoManualVisible = ref(false);
  5248. const recognitionBasicInfoExpanded = ref(false);
  5249. const recognitionBasicInfoRows = ref<RecognitionBasicInfoRow[]>([]);
  5250. const recognitionBasicInfoRegex = /^\{(.*?)\}$/s;
  5251. const fixedRecognitionBasicInfoFieldBindings = {
  5252. personCertNo: '客户证件号码',
  5253. personName: '客户名称',
  5254. personCardNo: '本方卡号',
  5255. } as const;
  5256. const normalizedFixedRecognitionBasicInfoFieldMap = Object.entries(fixedRecognitionBasicInfoFieldBindings).reduce(
  5257. (acc, [fieldKey, targetField]) => {
  5258. const normalizedFieldKey = normalizeFieldMatchText(fieldKey);
  5259. const normalizedTargetField = normalizeFieldMatchText(targetField);
  5260. if (normalizedFieldKey) {
  5261. acc[normalizedFieldKey] = targetField;
  5262. }
  5263. if (normalizedTargetField) {
  5264. acc[normalizedTargetField] = targetField;
  5265. }
  5266. return acc;
  5267. },
  5268. {} as Record<string, string>,
  5269. );
  5270. const requiredTotal = computed(() => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item)).length);
  5271. const requiredMatchedCount = computed(
  5272. () => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item) && isTargetMatched(item.key)).length,
  5273. );
  5274. const matchedFieldCount = computed(() => fixedTargetFields.value.filter((item) => isTargetMatched(item.key)).length);
  5275. const showRecognitionBasicInfoPanel = computed(
  5276. () => !!recognitionBasicInfoBaseFuncRegx.value || recognitionBasicInfoRows.value.length > 0,
  5277. );
  5278. const parseRecognitionDirectionEntries = (value: unknown): RecognitionDirectionItem[] => {
  5279. const rawText = normalizeRecognitionConfigText(value);
  5280. if (!rawText) return [];
  5281. try {
  5282. let parsedValue: Record<string, any> | null = null;
  5283. try {
  5284. parsedValue = JSON.parse(rawText);
  5285. } catch (jsonError) {
  5286. void jsonError;
  5287. const normalizedText = rawText
  5288. .replace(/([{,]\s*)'([^']+?)'\s*:/g, '$1"$2":')
  5289. .replace(/:\s*'([^']*?)'(\s*[,}])/g, ': "$1"$2');
  5290. parsedValue = JSON.parse(normalizedText);
  5291. }
  5292. if (!parsedValue || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) return [];
  5293. return Object.entries(parsedValue).reduce((acc, [label, itemValue], index) => {
  5294. const resolvedLabel = String(label || '').trim();
  5295. if (!resolvedLabel) return acc;
  5296. acc.push({
  5297. key: `${resolvedLabel}_${index}`,
  5298. label: resolvedLabel,
  5299. value: itemValue === null || itemValue === undefined ? '' : String(itemValue),
  5300. });
  5301. return acc;
  5302. }, [] as RecognitionDirectionItem[]);
  5303. } catch (error) {
  5304. console.error('解析 directionConf 失败', error);
  5305. return [];
  5306. }
  5307. };
  5308. const buildRecognitionDirectionConf = (entries?: RecognitionDirectionItem[]) => {
  5309. if (!entries?.length) return '';
  5310. const payload = entries.reduce(
  5311. (acc, item) => {
  5312. const label = normalizeRecognitionConfigText(item.label);
  5313. if (!label) return acc;
  5314. acc[label] = normalizeRecognitionConfigText(item.value);
  5315. return acc;
  5316. },
  5317. {} as Record<string, string>,
  5318. );
  5319. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  5320. };
  5321. const resolveRecognitionDirectionTab = (
  5322. fieldLabel: string,
  5323. entries: RecognitionDirectionItem[] = [],
  5324. ): RecognitionRuleTabKey | '' => {
  5325. const mergedText = `${fieldLabel}|${entries.map((item) => item.label).join('|')}`;
  5326. if (fieldLabel.includes('借贷') || /(收入|支出|借方|贷方)/.test(mergedText)) {
  5327. return '借贷标志';
  5328. }
  5329. if (/(通话|短信|通信|话单)/.test(fieldLabel) || /(主叫|被叫|发送|接收|呼入|呼出|上行|下行)/.test(mergedText)) {
  5330. return '通信类别';
  5331. }
  5332. return '';
  5333. };
  5334. const resolveRecognitionDirectionFields = (
  5335. node?: TreeFileInfo | null,
  5336. options: { preferTemplate?: boolean; currentFields?: RecognitionDirectionField[] } = {},
  5337. ) => {
  5338. const currentFieldMap = (options.currentFields || []).reduce(
  5339. (acc, item) => {
  5340. acc[item.key] = item;
  5341. return acc;
  5342. },
  5343. {} as Record<string, RecognitionDirectionField>,
  5344. );
  5345. const tableRuleMap = normalizeTableRuleList(node?.tableRuleList).reduce(
  5346. (acc, item, index) => {
  5347. acc[getTableRuleTargetKey(item, index)] = item;
  5348. return acc;
  5349. },
  5350. {} as Record<string, TreeTableRuleInfo>,
  5351. );
  5352. return fixedTargetFields.value.reduce((acc, field, index) => {
  5353. const templateFieldRecord = (resolveTemplateFieldMeta(field, index) || {}) as Record<string, any>;
  5354. const ruleFieldRecord = tableRuleMap[field.key] || null;
  5355. const currentFieldRecord = currentFieldMap[field.key];
  5356. const templateDirectionConf = normalizeRecognitionConfigText(
  5357. templateFieldRecord.directionConf ?? templateFieldRecord.direction_conf,
  5358. );
  5359. const ruleDirectionConf = normalizeRecognitionConfigText(ruleFieldRecord?.directionConf);
  5360. const currentDirectionConf = currentFieldRecord?.entries?.length
  5361. ? buildRecognitionDirectionConf(currentFieldRecord.entries)
  5362. : '';
  5363. const directionConfText = currentDirectionConf
  5364. ? currentDirectionConf
  5365. : options.preferTemplate
  5366. ? templateDirectionConf
  5367. : ruleDirectionConf || templateDirectionConf;
  5368. const baseEntries = currentFieldRecord?.entries?.length
  5369. ? currentFieldRecord.entries.map((entry) => ({ ...entry }))
  5370. : parseRecognitionDirectionEntries(directionConfText);
  5371. const baseTab = currentFieldRecord?.tab || resolveRecognitionDirectionTab(field.label, baseEntries);
  5372. const entries = baseTab
  5373. ? buildRecognitionDirectionEntriesByMappedColumn(field, baseTab, baseEntries)
  5374. : baseEntries;
  5375. const tab = currentFieldRecord?.tab || resolveRecognitionDirectionTab(field.label, entries);
  5376. if (!entries.length || !tab) return acc;
  5377. acc.push({
  5378. key: field.key,
  5379. label: field.label,
  5380. tab,
  5381. entries,
  5382. });
  5383. return acc;
  5384. }, [] as RecognitionDirectionField[]);
  5385. };
  5386. const applyRecognitionDirectionState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  5387. recognitionDirectionFields.value = resolveRecognitionDirectionFields(node, options);
  5388. };
  5389. const activeRecognitionDirectionFields = computed(() =>
  5390. recognitionDirectionFields.value.filter((item) => item.tab === activeRecognitionRule.value),
  5391. );
  5392. const hasRecognitionLoanTab = computed(() =>
  5393. recognitionDirectionFields.value.some((item) => item.entries.length && item.tab === '借贷标志'),
  5394. );
  5395. const hasRecognitionCommunicationTab = computed(() =>
  5396. recognitionDirectionFields.value.some((item) => item.entries.length && item.tab === '通信类别'),
  5397. );
  5398. const splitRecognitionDirectionKeywords = (value?: string) =>
  5399. normalizeRecognitionConfigText(value)
  5400. .split(/[\n,,;;、]+/)
  5401. .map((item) => item.trim())
  5402. .filter(Boolean);
  5403. const normalizeRecognitionDirectionGroup = (value: string, tab: RecognitionRuleTabKey) => {
  5404. if (tab === '借贷标志') {
  5405. return normalizeLoanFlagCoreValue(value);
  5406. }
  5407. const normalized = normalizeCallTypeCoreValue(value);
  5408. if (['主叫', '发送', '呼出', '上行'].includes(normalized)) {
  5409. return '主叫';
  5410. }
  5411. if (['被叫', '接收', '呼入', '下行'].includes(normalized)) {
  5412. return '被叫';
  5413. }
  5414. return '';
  5415. };
  5416. const mergeRecognitionDirectionKeywords = (currentValue = '', nextValues: string[] = []) => {
  5417. const mergedValues: string[] = [];
  5418. const normalizedValueSet = new Set<string>();
  5419. [...splitRecognitionDirectionKeywords(currentValue), ...nextValues].forEach((item) => {
  5420. const text = normalizeRecognitionConfigText(item);
  5421. const normalizedText = normalizeFieldMatchText(text);
  5422. if (!text || !normalizedText || normalizedValueSet.has(normalizedText)) return;
  5423. normalizedValueSet.add(normalizedText);
  5424. mergedValues.push(text);
  5425. });
  5426. return mergedValues.join(',');
  5427. };
  5428. const getRecognitionDirectionSampleValues = (fieldKey: string) => {
  5429. const mappedHeader = normalizeRuleText(effectiveFieldMapping.value[fieldKey] || fieldMapping.value[fieldKey]);
  5430. if (!mappedHeader) return [] as string[];
  5431. const matchedHeader = rightTableHeaders.value.find((item) => normalizeRuleText(item) === mappedHeader);
  5432. if (!matchedHeader) return [] as string[];
  5433. const collectedValues: string[] = [];
  5434. const normalizedValueSet = new Set<string>();
  5435. sourceTableRows.value.forEach((row) => {
  5436. const rawValue = normalizeRecognitionConfigText(row?.[matchedHeader]);
  5437. const normalizedValue = normalizeFieldMatchText(rawValue);
  5438. if (!rawValue || !normalizedValue || normalizedValueSet.has(normalizedValue)) return;
  5439. normalizedValueSet.add(normalizedValue);
  5440. collectedValues.push(rawValue);
  5441. });
  5442. return collectedValues;
  5443. };
  5444. const buildRecognitionDirectionEntriesByMappedColumn = (
  5445. field: Pick<RecognitionDirectionField, 'key' | 'label'>,
  5446. tab: RecognitionRuleTabKey,
  5447. entries: RecognitionDirectionItem[] = [],
  5448. ) => {
  5449. const baseEntries = entries.map((item, index) => ({
  5450. key: item.key || `${field.key}_${index}`,
  5451. label: normalizeRecognitionConfigText(item.label),
  5452. value: normalizeRecognitionConfigText(item.value),
  5453. }));
  5454. if (baseEntries.some((item) => item.label)) {
  5455. return baseEntries.filter((item) => item.label);
  5456. }
  5457. const sampleValues = getRecognitionDirectionSampleValues(field.key);
  5458. if (!sampleValues.length) {
  5459. return baseEntries.filter((item) => item.label);
  5460. }
  5461. const groupedKeywordMap = sampleValues.reduce(
  5462. (acc, value) => {
  5463. const groupLabel = normalizeRecognitionDirectionGroup(value, tab);
  5464. if (!groupLabel) return acc;
  5465. const coreKeyword = tab === '借贷标志' ? normalizeLoanFlagCoreValue(value) : normalizeCallTypeCoreValue(value);
  5466. const nextKeywords = [coreKeyword, value].filter((item): item is string => !!item);
  5467. acc[groupLabel] = mergeRecognitionDirectionKeywords((acc[groupLabel] || []).join(','), nextKeywords)
  5468. .split(/[\n,,;;、]+/)
  5469. .map((item) => item.trim())
  5470. .filter(Boolean);
  5471. return acc;
  5472. },
  5473. {} as Record<string, string[]>,
  5474. );
  5475. if (!Object.keys(groupedKeywordMap).length) {
  5476. return baseEntries.filter((item) => item.label);
  5477. }
  5478. const nextEntries = Object.keys(groupedKeywordMap).map((label, index) => ({
  5479. key: `${field.key}_${index}`,
  5480. label,
  5481. value: '',
  5482. }));
  5483. const groupEntryIndexMap = nextEntries.reduce(
  5484. (acc, item, index) => {
  5485. const groupLabel =
  5486. normalizeRecognitionDirectionGroup(item.label, tab) || normalizeRecognitionConfigText(item.label);
  5487. if (groupLabel && acc[groupLabel] === undefined) {
  5488. acc[groupLabel] = index;
  5489. }
  5490. return acc;
  5491. },
  5492. {} as Record<string, number>,
  5493. );
  5494. Object.entries(groupedKeywordMap).forEach(([groupLabel, keywords]) => {
  5495. let targetIndex = groupEntryIndexMap[groupLabel];
  5496. if (targetIndex === undefined) {
  5497. targetIndex = nextEntries.length;
  5498. nextEntries.push({
  5499. key: `${field.key}_${targetIndex}`,
  5500. label: groupLabel,
  5501. value: '',
  5502. });
  5503. groupEntryIndexMap[groupLabel] = targetIndex;
  5504. }
  5505. nextEntries[targetIndex].value = mergeRecognitionDirectionKeywords(nextEntries[targetIndex].value, keywords);
  5506. });
  5507. return nextEntries.filter((item) => item.label);
  5508. };
  5509. const buildRecognitionDirectionComparableSet = (
  5510. fields: RecognitionDirectionField[],
  5511. ): RecognitionDirectionComparableMap => {
  5512. const initialSet = {
  5513. 借贷标志: new Set<string>(),
  5514. 通信类别: new Set<string>(),
  5515. };
  5516. fields.forEach((field) => {
  5517. const currentSet = initialSet[field.tab];
  5518. field.entries.forEach((entry) => {
  5519. [entry.label, ...splitRecognitionDirectionKeywords(entry.value)].forEach((keyword) => {
  5520. const normalizedKeyword = normalizeRecognitionConfigText(keyword);
  5521. if (normalizedKeyword) {
  5522. currentSet.add(normalizedKeyword);
  5523. }
  5524. });
  5525. });
  5526. });
  5527. return initialSet;
  5528. };
  5529. const recognitionDirectionComparableSetByTab = computed<RecognitionDirectionComparableMap>(() =>
  5530. buildRecognitionDirectionComparableSet(recognitionDirectionFields.value),
  5531. );
  5532. const recognitionDirectionComparableSetByFieldKey = computed<Record<string, RecognitionDirectionComparableMap>>(() =>
  5533. recognitionDirectionFields.value.reduce(
  5534. (acc, field) => {
  5535. acc[field.key] = buildRecognitionDirectionComparableSet([field]);
  5536. return acc;
  5537. },
  5538. {} as Record<string, RecognitionDirectionComparableMap>,
  5539. ),
  5540. );
  5541. const matchRecognitionDirectionConfigValue = (value: string, tab: RecognitionRuleTabKey, targetKey = '') => {
  5542. const keywordSet =
  5543. (targetKey ? recognitionDirectionComparableSetByFieldKey.value[targetKey]?.[tab] : undefined) ||
  5544. recognitionDirectionComparableSetByTab.value[tab];
  5545. if (!keywordSet.size) return null;
  5546. const normalizedValue = normalizeRecognitionConfigText(value);
  5547. if (normalizedValue && keywordSet.has(normalizedValue)) {
  5548. return true;
  5549. }
  5550. return false;
  5551. };
  5552. const recognitionHeaderPreviewText = computed(() => {
  5553. if (headerRowIndex.value === null) {
  5554. return '未设置表头行,暂无法分析表头上方内容。';
  5555. }
  5556. const previewHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
  5557. const previewRows = sourceTableRows.value.slice(0, headerRowIndex.value);
  5558. const content = previewRows
  5559. .map((row) =>
  5560. previewHeaders
  5561. .map((header) => normalizeSingleLineText(row[header]))
  5562. .filter(Boolean)
  5563. .join(''),
  5564. )
  5565. .filter(Boolean)
  5566. .join('');
  5567. return content || '当前表头行上方暂无可识别内容。';
  5568. });
  5569. const recognitionHeaderPreviewMeta = computed(() => {
  5570. if (headerRowIndex.value === null) return '待识别';
  5571. if (headerRowIndex.value <= 0) return '上方 0 行';
  5572. return `上方 ${headerRowIndex.value} 行`;
  5573. });
  5574. const normalizeRecognitionBasicInfoText = (value: unknown) => normalizeRecognitionConfigText(value).trim();
  5575. const stripRecognitionBasicInfoTrailingColon = (value: string) => value.replace(/[::]\s*$/g, '').trim();
  5576. const normalizeRecognitionBasicInfoStorageText = (value: unknown) =>
  5577. stripRecognitionBasicInfoTrailingColon(normalizeRecognitionBasicInfoText(value));
  5578. const resolveRecognitionBasicInfoTargetField = (value: unknown) => {
  5579. const normalizedValue = normalizeRecognitionBasicInfoStorageText(value);
  5580. if (!normalizedValue) return '';
  5581. return normalizedFixedRecognitionBasicInfoFieldMap[normalizeFieldMatchText(normalizedValue)] || normalizedValue;
  5582. };
  5583. const parseRecognitionBasicInfoPayload = (funcRegx: unknown) => {
  5584. const rawText = normalizeRecognitionBasicInfoText(funcRegx);
  5585. if (!rawText) return null;
  5586. const matchedText = rawText.match(recognitionBasicInfoRegex)?.[0];
  5587. if (!matchedText) return null;
  5588. try {
  5589. const parsedValue = JSON.parse(matchedText) as Record<string, any>;
  5590. if (!parsedValue || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
  5591. return null;
  5592. }
  5593. return parsedValue;
  5594. } catch (error) {
  5595. console.error('解析基本信息配置失败', error);
  5596. return null;
  5597. }
  5598. };
  5599. const buildRecognitionBasicInfoTargetFieldByHeaderMatchMap = (funcRegx: unknown) => {
  5600. const parsedValue = parseRecognitionBasicInfoPayload(funcRegx);
  5601. if (!parsedValue) return {} as Record<string, string>;
  5602. return Object.entries(parsedValue).reduce(
  5603. (acc, [targetField, rawValue]) => {
  5604. const rawList = Array.isArray(rawValue) ? rawValue : [];
  5605. const headerMatch = normalizeRecognitionBasicInfoText(rawList[0] ?? '');
  5606. const normalizedHeaderMatch = normalizeRecognitionBasicInfoStorageText(headerMatch);
  5607. if (normalizedHeaderMatch) {
  5608. acc[normalizedHeaderMatch] = resolveRecognitionBasicInfoTargetField(targetField);
  5609. }
  5610. return acc;
  5611. },
  5612. {} as Record<string, string>,
  5613. );
  5614. };
  5615. const parseRecognitionBasicInfoRows = (funcRegx: unknown, referenceFuncRegx?: unknown): RecognitionBasicInfoRow[] => {
  5616. const parsedValue = parseRecognitionBasicInfoPayload(funcRegx);
  5617. if (!parsedValue) return [];
  5618. const parentTargetFieldMap = buildRecognitionBasicInfoTargetFieldByHeaderMatchMap(referenceFuncRegx);
  5619. return Object.entries(parsedValue).reduce((acc, [storedKey, rawValue]) => {
  5620. const rawList = Array.isArray(rawValue) ? rawValue : [];
  5621. const headerMatch = normalizeRecognitionBasicInfoText(rawList[0] ?? '');
  5622. const normalizedHeaderMatch = normalizeRecognitionBasicInfoStorageText(headerMatch);
  5623. const endFlag = normalizeRecognitionBasicInfoText(rawList[1] ?? '');
  5624. const normalizedStoredKey = normalizeRecognitionBasicInfoStorageText(storedKey);
  5625. const resolvedStoredTargetField = resolveRecognitionBasicInfoTargetField(normalizedStoredKey);
  5626. const mappedParentTargetField =
  5627. normalizedHeaderMatch && normalizedStoredKey === normalizedHeaderMatch
  5628. ? parentTargetFieldMap[normalizedHeaderMatch]
  5629. : '';
  5630. const resolvedTargetField = mappedParentTargetField || resolvedStoredTargetField;
  5631. acc.push({
  5632. key: createRecognitionBasicInfoKey(),
  5633. headerMatch,
  5634. targetField: resolvedTargetField,
  5635. endFlag,
  5636. });
  5637. return acc;
  5638. }, [] as RecognitionBasicInfoRow[]);
  5639. };
  5640. const clearRecognitionBasicInfoFilledContent = (rows: RecognitionBasicInfoRow[]) =>
  5641. rows.map((item) => ({
  5642. ...item,
  5643. headerMatch: '',
  5644. endFlag: '',
  5645. }));
  5646. const hasRecognitionBasicInfoFilledContent = (rows: RecognitionBasicInfoRow[]) =>
  5647. rows.some(
  5648. (item) =>
  5649. !!normalizeRecognitionBasicInfoText(item.headerMatch) || !!normalizeRecognitionBasicInfoText(item.endFlag),
  5650. );
  5651. const hasOwnRecognitionBasicInfoConfig = (node?: TreeFileInfo | null) =>
  5652. !!node && !!normalizeRecognitionConfigText(node.funcRegx);
  5653. const isSameRecognitionBasicInfoConfig = (left?: unknown, right?: unknown) => {
  5654. const leftText = normalizeRecognitionConfigText(left);
  5655. const rightText = normalizeRecognitionConfigText(right);
  5656. return !!leftText && !!rightText && leftText === rightText;
  5657. };
  5658. const resolveRecognitionBasicInfoSource = (
  5659. node?: TreeFileInfo | null,
  5660. options: { preferTemplate?: boolean } = {},
  5661. ) => {
  5662. const templateFuncRegx = normalizeRecognitionConfigText(
  5663. templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
  5664. );
  5665. const hasNodeFuncRegx = hasOwnRecognitionBasicInfoConfig(node);
  5666. const nodeFuncRegx = normalizeRecognitionConfigText(node?.funcRegx);
  5667. if (options.preferTemplate) {
  5668. return templateFuncRegx;
  5669. }
  5670. if (hasNodeFuncRegx) {
  5671. return nodeFuncRegx;
  5672. }
  5673. return templateFuncRegx;
  5674. };
  5675. const applyRecognitionBasicInfoState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  5676. const templateId = normalizeTemplateId(
  5677. node?.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  5678. );
  5679. const parentTemplateId = getSelectedTemplateMainId(node?.mainId);
  5680. const parentTemplate = parentTemplateId
  5681. ? resolveTemplateInfoById(parentTemplateId)
  5682. : resolveParentTemplateInfoById(templateId);
  5683. const source = resolveRecognitionBasicInfoSource(node, options);
  5684. const hasNodeFuncRegx =
  5685. hasOwnRecognitionBasicInfoConfig(node) &&
  5686. !isSameRecognitionBasicInfoConfig(
  5687. node?.funcRegx,
  5688. templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
  5689. ) &&
  5690. !isSameRecognitionBasicInfoConfig(node?.funcRegx, parentTemplate?.funcRegx);
  5691. const shouldClearFilledContent = options.preferTemplate || !hasNodeFuncRegx;
  5692. recognitionBasicInfoBaseFuncRegx.value = source;
  5693. const parsedRows = parseRecognitionBasicInfoRows(source, parentTemplate?.funcRegx);
  5694. recognitionBasicInfoRows.value = shouldClearFilledContent
  5695. ? clearRecognitionBasicInfoFilledContent(parsedRows)
  5696. : parsedRows;
  5697. recognitionBasicInfoExpanded.value = !!recognitionBasicInfoRows.value.length;
  5698. };
  5699. const resolveRecognitionAmountZfEnabled = (
  5700. node?: TreeFileInfo | null,
  5701. options: { preferTemplate?: boolean } = {},
  5702. ) => {
  5703. const tableRule = findTransAmountTableRule(node?.tableRuleList);
  5704. const tableRuleZf = getRecordZfValue(tableRule);
  5705. const templateField = findTransAmountTemplateFieldMeta(templateDetail.value || activeTemplateItem.value);
  5706. const templateZf = getRecordZfValue(templateField);
  5707. if (options.preferTemplate) {
  5708. return templateZf !== undefined
  5709. ? getZfFlag(templateZf)
  5710. : tableRuleZf !== undefined
  5711. ? getZfFlag(tableRuleZf)
  5712. : false;
  5713. }
  5714. return tableRuleZf !== undefined
  5715. ? getZfFlag(tableRuleZf)
  5716. : templateZf !== undefined
  5717. ? getZfFlag(templateZf)
  5718. : false;
  5719. };
  5720. const applyRecognitionAmountZfState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  5721. recognitionAmountZfEnabled.value = resolveRecognitionAmountZfEnabled(node, options);
  5722. };
  5723. const handleRecognitionAmountZfChange = (checked: unknown) => {
  5724. const enabled = getZfFlag(checked);
  5725. recognitionAmountZfEnabled.value = enabled;
  5726. if (mappingPopoverOpenKey.value && isTargetHandledByAmountZf(mappingPopoverOpenKey.value)) {
  5727. mappingPopoverOpenKey.value = '';
  5728. originFilterKeyword.value = '';
  5729. }
  5730. saveCurrentNodeState();
  5731. runRulePipeline();
  5732. createMessage.success(enabled ? '正负标识识别已开启' : '正负标识识别已关闭');
  5733. };
  5734. const buildRecognitionBasicInfoFuncRegx = () => {
  5735. if (!recognitionBasicInfoRows.value.length) {
  5736. return recognitionBasicInfoBaseFuncRegx.value;
  5737. }
  5738. if (!hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value)) {
  5739. return '';
  5740. }
  5741. const payload = recognitionBasicInfoRows.value.reduce(
  5742. (acc, item) => {
  5743. const targetField = resolveRecognitionBasicInfoTargetField(item.targetField);
  5744. if (!targetField) return acc;
  5745. acc[targetField] = [
  5746. normalizeRecognitionBasicInfoText(item.headerMatch),
  5747. normalizeRecognitionBasicInfoText(item.endFlag),
  5748. ];
  5749. return acc;
  5750. },
  5751. {} as Record<string, string[]>,
  5752. );
  5753. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  5754. };
  5755. const handleClearRecognitionDirection = () => {
  5756. const currentTab = activeRecognitionRule.value as RecognitionRuleTabKey;
  5757. if (!currentTab) return;
  5758. recognitionDirectionFields.value = recognitionDirectionFields.value.map((item) => {
  5759. if (item.tab !== currentTab) return item;
  5760. return {
  5761. ...item,
  5762. entries: item.entries.map((entry) => ({
  5763. ...entry,
  5764. value: '',
  5765. })),
  5766. };
  5767. });
  5768. };
  5769. const handleSaveRecognitionDirection = () => {
  5770. if (!activeRecognitionDirectionFields.value.length) return;
  5771. saveCurrentNodeState();
  5772. runRulePipeline();
  5773. createMessage.success(`${activeRecognitionRule.value}配置已保存`);
  5774. };
  5775. const refreshRecognitionDirectionState = (node?: TreeFileInfo | null) => {
  5776. recognitionDirectionFields.value = resolveRecognitionDirectionFields(node, {
  5777. currentFields: recognitionDirectionFields.value,
  5778. });
  5779. };
  5780. const getMappingStatusClass = (item: Pick<MappingTargetField, 'key' | 'label' | 'required'>) => {
  5781. if (isTargetMatched(item.key)) return 'is-matched';
  5782. if (isMappingTargetRequired(item)) return 'is-required';
  5783. return 'is-optional';
  5784. };
  5785. const getFieldMatchScore = (targetLabel: string, header: string) => {
  5786. const rawTarget = normalizeRuleText(targetLabel);
  5787. const rawHeader = normalizeRuleText(header);
  5788. if (!rawTarget || !rawHeader) return -1;
  5789. if (rawTarget === rawHeader) return 1000;
  5790. const normalizedTarget = normalizeFieldMatchText(rawTarget);
  5791. const normalizedHeader = normalizeFieldMatchText(rawHeader);
  5792. if (!normalizedTarget || !normalizedHeader) return -1;
  5793. if (normalizedTarget === normalizedHeader) return 900;
  5794. return -1;
  5795. };
  5796. const findBestMatchedHeader = (targetLabel: string, headers: string[], usedHeaders: Set<string>) => {
  5797. let bestHeader = '';
  5798. let bestScore = -1;
  5799. headers.forEach((header, index) => {
  5800. if (usedHeaders.has(header)) return;
  5801. const score = getFieldMatchScore(targetLabel, header);
  5802. if (score < 0) return;
  5803. if (score > bestScore) {
  5804. bestScore = score;
  5805. bestHeader = header;
  5806. return;
  5807. }
  5808. if (score === bestScore && bestHeader) {
  5809. const currentDiff = Math.abs(
  5810. normalizeFieldMatchText(bestHeader).length - normalizeFieldMatchText(targetLabel).length,
  5811. );
  5812. const nextDiff = Math.abs(normalizeFieldMatchText(header).length - normalizeFieldMatchText(targetLabel).length);
  5813. if (nextDiff < currentDiff) {
  5814. bestHeader = header;
  5815. return;
  5816. }
  5817. if (nextDiff === currentDiff) {
  5818. const currentIndex = headers.findIndex((item) => item === bestHeader);
  5819. if (currentIndex < 0 || index < currentIndex) {
  5820. bestHeader = header;
  5821. }
  5822. }
  5823. }
  5824. });
  5825. return bestHeader;
  5826. };
  5827. const autoMatchFields = (showMessage = true) => {
  5828. const headers = [...rightTableHeaders.value];
  5829. const usedHeaders = new Set<string>();
  5830. const lockedMapping = Object.entries(fieldMapping.value).reduce(
  5831. (acc, [targetKey, header]) => {
  5832. if (isTargetHandledByRecognitionRule(targetKey) && header) {
  5833. acc[targetKey] = header;
  5834. }
  5835. return acc;
  5836. },
  5837. {} as Record<string, string>,
  5838. );
  5839. const nextMapping = fixedTargetFields.value.reduce((acc, item) => {
  5840. if (isTargetHandledByRecognitionRule(item.key)) {
  5841. return acc;
  5842. }
  5843. const bestHeader = findBestMatchedHeader(item.label, headers, usedHeaders);
  5844. acc[item.key] = bestHeader || '';
  5845. if (bestHeader) {
  5846. usedHeaders.add(bestHeader);
  5847. }
  5848. return acc;
  5849. }, lockedMapping);
  5850. fieldMapping.value = nextMapping;
  5851. refreshRecognitionDirectionState(activeNodeInfo.value);
  5852. if (showMessage) {
  5853. const matchedCount = fixedTargetFields.value.filter(
  5854. (item) => isTargetHandledByRecognitionRule(item.key) || !!nextMapping[item.key],
  5855. ).length;
  5856. createMessage.success(`已完成智能预匹配,共匹配 ${matchedCount} 项`);
  5857. }
  5858. };
  5859. const clearFieldMapping = () => {
  5860. fieldMapping.value = {};
  5861. refreshRecognitionDirectionState(activeNodeInfo.value);
  5862. };
  5863. const applyNodePreset = async (item?: TreeFileInfo | null) => {
  5864. const previewDataset = await loadPreviewDatasetFromFile(item);
  5865. const hasPresetHeaders = previewDataset ? applyPreviewDataset(previewDataset) : applyPresetHeadersFromNode(item);
  5866. restoreStoredColumnRules(item?.tableRuleList);
  5867. applyRecognitionDirectionState(item);
  5868. applyRecognitionBasicInfoState(item);
  5869. applyRecognitionAmountZfState(item);
  5870. childTemplateName.value = normalizeRuleText(item?.childTemplateName);
  5871. const headerRow = normalizeStoredLineNoToRowIndex(item?.lineNo);
  5872. if (headerRow !== null && !hasPresetHeaders) {
  5873. applyHeaderRow(headerRow, { silent: true, autoMatch: false });
  5874. } else {
  5875. headerRowIndex.value = headerRow;
  5876. selectedRowIndex.value = headerRow;
  5877. syncHeaderRowInput();
  5878. }
  5879. if (!applyTableRuleMapping(item?.tableRuleList)) {
  5880. autoMatchFields(false);
  5881. }
  5882. restoreStoredFunRules(item?.tableRuleList);
  5883. restoreFileLevelRules(item);
  5884. runRulePipeline();
  5885. };
  5886. type TemplateTab = 'template' | 'plugin';
  5887. const DEFAULT_TEMPLATE_CATEGORY = '全部分类';
  5888. const FALLBACK_TEMPLATE_CATEGORY = '未分类';
  5889. const templateModalOpen = ref(false);
  5890. const activeTemplateTab = ref<TemplateTab>('template');
  5891. const activeCategory = ref(DEFAULT_TEMPLATE_CATEGORY);
  5892. const templateKeyword = ref('');
  5893. const templateSource = ref<number>(1);
  5894. const templateList = ref<TemplateListItem[]>(mergeTemplateCollections(mockTemplateList));
  5895. const templateCategoryOptions = ref<string[]>([DEFAULT_TEMPLATE_CATEGORY]);
  5896. const activeTemplateItem = ref<TemplateListItem | null>(null);
  5897. const modalTemplateItem = ref<TemplateListItem | null>(null);
  5898. const modalTemplateDetail = ref<TableInfo | null>(null);
  5899. const templatePreviewLoading = ref(false);
  5900. const templateListLoading = ref(false);
  5901. const templateListLoaded = ref(templateList.value.length > 0);
  5902. const templateDetailCache = new Map<number, TableInfo>();
  5903. let templatePreviewRequestToken = 0;
  5904. const templateSourceOptions = [
  5905. { label: '全部', value: 0 },
  5906. { label: '系统内置', value: 1 },
  5907. { label: '新创建子模板', value: 2 },
  5908. ];
  5909. const categories = computed(() => {
  5910. const set = new Set<string>(
  5911. templateCategoryOptions.value.length ? templateCategoryOptions.value : [DEFAULT_TEMPLATE_CATEGORY],
  5912. );
  5913. templateList.value.forEach((t) => set.add(t.classify || FALLBACK_TEMPLATE_CATEGORY));
  5914. return Array.from(set);
  5915. });
  5916. const pluginList = ref<TemplateListItem[]>([
  5917. {
  5918. id: -1,
  5919. tableNameCn: '工行账单特殊处理',
  5920. tableNameEn: 'plug',
  5921. classify: '金融类',
  5922. headers: '插件模板',
  5923. } as TableInfo,
  5924. ]);
  5925. const pluginCategorySet = computed(() => {
  5926. const set = new Set<string>();
  5927. pluginList.value.forEach((item) => {
  5928. const category = normalizeTemplateClassifyText(item.classify) || FALLBACK_TEMPLATE_CATEGORY;
  5929. set.add(category);
  5930. });
  5931. return set;
  5932. });
  5933. const showTemplateTabSwitch = computed(
  5934. () => activeCategory.value === DEFAULT_TEMPLATE_CATEGORY || pluginCategorySet.value.has(activeCategory.value),
  5935. );
  5936. const isPluginTemplateTab = computed(() => activeTemplateTab.value === 'plugin');
  5937. const isSystemTemplateRecord = (record?: Partial<TemplateListItem> | null) => {
  5938. const normalizedValue = String(record?.hasInner ?? '')
  5939. .trim()
  5940. .toUpperCase();
  5941. return ['YES', '1', 'TRUE'].includes(normalizedValue);
  5942. };
  5943. const isChildTemplateRecord = (record?: Partial<TemplateListItem> | null) =>
  5944. !isSystemTemplateRecord(record) && normalizeTemplateId(record?.mainId) > 0;
  5945. const templateSearchBaseList = computed(() => {
  5946. const baseList = activeTemplateTab.value === 'plugin' ? pluginList.value : templateList.value;
  5947. return baseList.filter((tpl) => {
  5948. const passKeyword =
  5949. !templateKeyword.value || `${tpl.tableNameCn || ''}${tpl.tableNameEn || ''}`.includes(templateKeyword.value);
  5950. const passSource =
  5951. templateSource.value === 0 ||
  5952. (templateSource.value === 1 && isSystemTemplateRecord(tpl)) ||
  5953. (templateSource.value === 2 && isChildTemplateRecord(tpl));
  5954. return passKeyword && passSource;
  5955. });
  5956. });
  5957. const filteredTemplateList = computed(() => {
  5958. return templateSearchBaseList.value.filter((tpl) => {
  5959. return (
  5960. activeTemplateTab.value === 'plugin' ||
  5961. activeCategory.value === DEFAULT_TEMPLATE_CATEGORY ||
  5962. (tpl.classify || FALLBACK_TEMPLATE_CATEGORY) === activeCategory.value
  5963. );
  5964. });
  5965. });
  5966. const templateCategoryCountMap = computed<Record<string, number>>(() => {
  5967. return templateSearchBaseList.value.reduce(
  5968. (acc, tpl) => {
  5969. const category = tpl.classify || FALLBACK_TEMPLATE_CATEGORY;
  5970. acc[category] = (acc[category] || 0) + 1;
  5971. acc[DEFAULT_TEMPLATE_CATEGORY] = (acc[DEFAULT_TEMPLATE_CATEGORY] || 0) + 1;
  5972. return acc;
  5973. },
  5974. {} as Record<string, number>,
  5975. );
  5976. });
  5977. const getTemplateDisplayName = (tpl?: Partial<TemplateListItem> | null) => {
  5978. return tpl?.tableNameCn || tpl?.tableNameEn || '未命名模板';
  5979. };
  5980. const getTemplateSourceLabel = (tpl?: Partial<TemplateListItem> | null) => {
  5981. if (Number(tpl?.id) < 0) return '模板插件';
  5982. return isSystemTemplateRecord(tpl) ? '系统内置' : '新创建子模板';
  5983. };
  5984. const getTemplateHeaderList = (tpl?: Partial<TableInfo> | null) => normalizeTemplateHeaders((tpl as any)?.headers);
  5985. const getTemplateHeaderCount = (tpl?: Partial<TableInfo> | null) => getTemplateHeaderList(tpl).length;
  5986. const getCategoryCount = (category: string) => {
  5987. if (category === DEFAULT_TEMPLATE_CATEGORY) return templateCategoryCountMap.value[DEFAULT_TEMPLATE_CATEGORY] || 0;
  5988. return templateCategoryCountMap.value[category] || 0;
  5989. };
  5990. const resolveTemplateClassifyRecords = (payload: unknown): unknown[] => {
  5991. if (Array.isArray(payload)) return payload;
  5992. if (!payload || typeof payload !== 'object') return [];
  5993. const record = payload as Record<string, unknown>;
  5994. for (const key of ['data', 'list', 'records', 'result']) {
  5995. const value = record[key];
  5996. if (Array.isArray(value)) return value;
  5997. }
  5998. return [];
  5999. };
  6000. const normalizeTemplateClassifyValue = (item: unknown) => {
  6001. if (item === null || item === undefined) return '';
  6002. if (typeof item === 'string' || typeof item === 'number') {
  6003. return normalizeTemplateClassifyText(item);
  6004. }
  6005. if (typeof item !== 'object') return '';
  6006. const record = item as Record<string, unknown>;
  6007. return normalizeTemplateClassifyText(
  6008. record.value ?? record.classify ?? record.name ?? record.label ?? record.dictValue ?? record.dictLabel,
  6009. );
  6010. };
  6011. const loadTemplateCategories = async () => {
  6012. try {
  6013. const response = await tplCategoryList();
  6014. const categorySet = new Set<string>([DEFAULT_TEMPLATE_CATEGORY]);
  6015. resolveTemplateClassifyRecords(response).forEach((item) => {
  6016. const value = normalizeTemplateClassifyValue(item);
  6017. if (value && value !== DEFAULT_TEMPLATE_CATEGORY) {
  6018. categorySet.add(value);
  6019. }
  6020. });
  6021. templateCategoryOptions.value = Array.from(categorySet);
  6022. } catch (error) {
  6023. templateCategoryOptions.value = [DEFAULT_TEMPLATE_CATEGORY];
  6024. }
  6025. };
  6026. const getTemplateListItemById = (templateId?: string | number | null) => {
  6027. const normalizedId = normalizeTemplateId(templateId);
  6028. if (!normalizedId) return null;
  6029. return templateList.value.find((item) => normalizeTemplateId(item.id) === normalizedId) || null;
  6030. };
  6031. const applyTableRuleRequiredFlags = (fields: MappingTargetField[], tableRules: TreeTableRuleInfo[]) => {
  6032. if (!tableRules.length) return fields;
  6033. const ruleMapByKey = tableRules.reduce(
  6034. (acc, item, index) => {
  6035. const key = getTableRuleTargetKey(item, index);
  6036. if (key) acc[key] = item;
  6037. return acc;
  6038. },
  6039. {} as Record<string, TreeTableRuleInfo>,
  6040. );
  6041. const ruleMapByLabel = tableRules.reduce(
  6042. (acc, item, index) => {
  6043. const label = getTableRuleTargetLabel(item, index);
  6044. if (label) acc[label] = item;
  6045. return acc;
  6046. },
  6047. {} as Record<string, TreeTableRuleInfo>,
  6048. );
  6049. return fields.map((item) => {
  6050. const rule = ruleMapByKey[item.key] || ruleMapByLabel[item.label];
  6051. return {
  6052. ...item,
  6053. required: rule ? getTableRuleRequiredFlag(rule, item.required) : item.required,
  6054. };
  6055. });
  6056. };
  6057. const resolveNodeTargetFields = (item?: TreeFileInfo | null, preferredTemplateId?: string | number | null) => {
  6058. const tableRules = normalizeTableRuleList(item?.tableRuleList);
  6059. const templateId = normalizeTemplateId(preferredTemplateId ?? item?.templateId);
  6060. const template = (templateId && (getTemplateListItemById(templateId) || mockTemplateMap[templateId])) || null;
  6061. const baseFields = tableRules.length
  6062. ? buildTargetFieldsFromRuleList(tableRules)
  6063. : buildTargetFieldsFromTemplateInfo(template);
  6064. return applyTableRuleRequiredFlags(baseFields, tableRules);
  6065. };
  6066. const getRequiredMatchStats = (
  6067. fields: MappingTargetField[],
  6068. mapping: Record<string, string>,
  6069. recognitionRows: RecognitionBasicInfoRow[] = [],
  6070. amountZfEnabled = false,
  6071. ) => {
  6072. const matchedByRecognitionBasicInfo = buildRecognitionBasicInfoMatchedTargetKeySet(recognitionRows, fields);
  6073. const handledTargetKeySet = new Set(matchedByRecognitionBasicInfo);
  6074. buildAmountZfHandledTargetKeySet(fields, amountZfEnabled).forEach((key) => {
  6075. handledTargetKeySet.add(key);
  6076. });
  6077. const effectiveMapping = buildEffectiveFieldMapping(mapping, handledTargetKeySet);
  6078. const requiredFields = fields.filter((item) => item.required);
  6079. const requiredTotal = requiredFields.length;
  6080. const requiredMatched = requiredFields.filter(
  6081. (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
  6082. ).length;
  6083. const matchedCount = fields.filter(
  6084. (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
  6085. ).length;
  6086. return { requiredTotal, requiredMatched, matchedCount };
  6087. };
  6088. const getSheetNodeVisualState = (item?: TreeFileInfo | null): SheetNodeVisualState => {
  6089. if (!isTreeSheetNode(item)) return 'fail';
  6090. const nodeKey = getTreeNodeKey(item);
  6091. if (analyzingNodeKeys.value.includes(nodeKey)) {
  6092. return 'analyzing';
  6093. }
  6094. const defaultState = getFileStatusVisualState(item.fileStatus) || (isSheetMatchComplete(item) ? 'success' : 'fail');
  6095. const initialSignature = initialNodeStatusSignatureMap.get(nodeKey);
  6096. if (activeNodeKey.value === nodeKey) {
  6097. const currentState = buildNodeCacheState();
  6098. const hasEditedState = !initialSignature || buildNodeStatusSignature(currentState) !== initialSignature;
  6099. if (!hasEditedState) {
  6100. return defaultState;
  6101. }
  6102. return isMatchSuccessful(requiredTotal.value, requiredMatchedCount.value, matchedFieldCount.value)
  6103. ? 'success'
  6104. : 'fail';
  6105. }
  6106. const cached = nodeStateCache.get(nodeKey);
  6107. if (cached) {
  6108. const hasEditedState = !initialSignature || buildNodeStatusSignature(cached) !== initialSignature;
  6109. if (!hasEditedState) {
  6110. return defaultState;
  6111. }
  6112. const {
  6113. requiredTotal: cachedRequiredTotal,
  6114. requiredMatched: cachedRequiredMatched,
  6115. matchedCount: cachedMatchedCount,
  6116. } = getRequiredMatchStats(
  6117. resolveNodeTargetFields(item, cached.selectedTemplateId),
  6118. cached.fieldMapping,
  6119. cached.recognitionBasicInfoRows,
  6120. cached.recognitionAmountZfEnabled,
  6121. );
  6122. return isMatchSuccessful(cachedRequiredTotal, cachedRequiredMatched, cachedMatchedCount) ? 'success' : 'fail';
  6123. }
  6124. return defaultState;
  6125. };
  6126. const mergeTemplateListItem = (template?: TemplateListItem | TableInfo | null) => {
  6127. const templateId = normalizeTemplateId(template?.id);
  6128. if (!template || !templateId) return;
  6129. const nextItem = template as TemplateListItem;
  6130. const targetIndex = templateList.value.findIndex((item) => normalizeTemplateId(item.id) === templateId);
  6131. if (targetIndex >= 0) {
  6132. templateList.value[targetIndex] = {
  6133. ...templateList.value[targetIndex],
  6134. ...nextItem,
  6135. };
  6136. return;
  6137. }
  6138. templateList.value = [...templateList.value, nextItem];
  6139. };
  6140. const syncActiveTemplateItem = (preferredId?: string | number | null) => {
  6141. const preferredTemplateId = normalizeTemplateId(preferredId);
  6142. const currentTemplateId = normalizeTemplateId(activeTemplateItem.value?.id);
  6143. const currentBaseList = activeTemplateTab.value === 'plugin' ? pluginList.value : filteredTemplateList.value;
  6144. if (preferredTemplateId) {
  6145. const matched =
  6146. currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) ||
  6147. templateList.value.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) ||
  6148. null;
  6149. if (matched) {
  6150. activeTemplateItem.value = matched;
  6151. return matched;
  6152. }
  6153. }
  6154. if (currentTemplateId) {
  6155. const currentMatched =
  6156. currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) ||
  6157. templateList.value.find((item) => normalizeTemplateId(item.id) === currentTemplateId) ||
  6158. null;
  6159. if (currentMatched) {
  6160. activeTemplateItem.value = currentMatched;
  6161. return currentMatched;
  6162. }
  6163. }
  6164. activeTemplateItem.value = null;
  6165. return activeTemplateItem.value;
  6166. };
  6167. const syncModalTemplateItem = (preferredId?: string | number | null) => {
  6168. const currentBaseList = filteredTemplateList.value;
  6169. if (activeTemplateTab.value === 'plugin') {
  6170. const currentPluginId = modalTemplateItem.value?.id;
  6171. const matchedPlugin = currentBaseList.find((item) => String(item.id) === String(currentPluginId)) || null;
  6172. modalTemplateItem.value = matchedPlugin;
  6173. return modalTemplateItem.value;
  6174. }
  6175. const preferredTemplateId = normalizeTemplateId(preferredId);
  6176. const currentTemplateId = normalizeTemplateId(modalTemplateItem.value?.id);
  6177. if (preferredTemplateId) {
  6178. const matched = currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) || null;
  6179. if (matched) {
  6180. modalTemplateItem.value = matched;
  6181. return matched;
  6182. }
  6183. }
  6184. if (currentTemplateId) {
  6185. const currentMatched = currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) || null;
  6186. if (currentMatched) {
  6187. modalTemplateItem.value = currentMatched;
  6188. return currentMatched;
  6189. }
  6190. }
  6191. modalTemplateItem.value = currentBaseList[0] || null;
  6192. return modalTemplateItem.value;
  6193. };
  6194. const mergeTemplateInfo = (
  6195. base?: Partial<TemplateListItem> | Partial<TableInfo> | null,
  6196. detail?: Partial<TableInfo> | null,
  6197. ) => {
  6198. if (!base && !detail) return null;
  6199. return { ...(base || {}), ...(detail || {}) } as TemplateListItem & TableInfo;
  6200. };
  6201. const resolveTemplateInfoById = (templateId?: string | number | null) => {
  6202. const id = normalizeTemplateId(templateId);
  6203. if (!id) return null;
  6204. const activeDetail = normalizeTemplateId(templateDetail.value?.id) === id ? templateDetail.value : null;
  6205. const activeItem = normalizeTemplateId(activeTemplateItem.value?.id) === id ? activeTemplateItem.value : null;
  6206. const cachedDetail = templateDetailCache.get(id) || null;
  6207. const listItem = getTemplateListItemById(id) || mockTemplateMap[id] || null;
  6208. return (
  6209. mergeTemplateInfo(activeItem || listItem || activeDetail, activeDetail || cachedDetail) ||
  6210. activeDetail ||
  6211. cachedDetail ||
  6212. activeItem ||
  6213. listItem ||
  6214. null
  6215. );
  6216. };
  6217. const resolveParentTemplateInfoById = (templateId?: string | number | null) => {
  6218. const template = resolveTemplateInfoById(templateId);
  6219. const mainId = normalizeTemplateId((template as TableInfo | null)?.mainId);
  6220. return (mainId && resolveTemplateInfoById(mainId)) || template;
  6221. };
  6222. const getSelectedTemplateMainId = (fallbackMainId?: string | number | null) => {
  6223. if (templateDetail.value && Object.prototype.hasOwnProperty.call(templateDetail.value, 'mainId')) {
  6224. return normalizeTemplateId(templateDetail.value.mainId);
  6225. }
  6226. if (activeTemplateItem.value && Object.prototype.hasOwnProperty.call(activeTemplateItem.value, 'mainId')) {
  6227. return normalizeTemplateId(activeTemplateItem.value.mainId);
  6228. }
  6229. return normalizeTemplateId(fallbackMainId);
  6230. };
  6231. const syncModalTemplatePreviewState = (
  6232. template?: TemplateListItem | null,
  6233. options: { preferAppliedDetail?: boolean } = {},
  6234. ) => {
  6235. modalTemplateItem.value = template || null;
  6236. modalTemplateDetail.value = null;
  6237. templatePreviewLoading.value = false;
  6238. templatePreviewRequestToken += 1;
  6239. const id = normalizeTemplateId(template?.id);
  6240. if (!id || Number(template?.id) < 0) return null;
  6241. if (options.preferAppliedDetail && normalizeTemplateId(templateDetail.value?.id) === id && templateDetail.value) {
  6242. modalTemplateDetail.value = templateDetail.value;
  6243. return modalTemplateDetail.value;
  6244. }
  6245. const cachedDetail = templateDetailCache.get(id);
  6246. if (cachedDetail) {
  6247. modalTemplateDetail.value = cachedDetail;
  6248. return modalTemplateDetail.value;
  6249. }
  6250. return null;
  6251. };
  6252. const loadTemplatePreviewDetail = async (
  6253. template?: TemplateListItem | null,
  6254. options: { preferAppliedDetail?: boolean } = {},
  6255. ) => {
  6256. const existingDetail = syncModalTemplatePreviewState(template, options);
  6257. if (existingDetail) {
  6258. return existingDetail;
  6259. }
  6260. const id = normalizeTemplateId(template?.id);
  6261. if (!id || Number(template?.id) < 0) return null;
  6262. const requestToken = ++templatePreviewRequestToken;
  6263. templatePreviewLoading.value = true;
  6264. try {
  6265. const detail = await tplDetail({ id });
  6266. if (requestToken !== templatePreviewRequestToken) return null;
  6267. if (detail) {
  6268. templateDetailCache.set(id, detail);
  6269. modalTemplateDetail.value = detail;
  6270. mergeTemplateListItem(mergeTemplateInfo(template, detail));
  6271. modalTemplateItem.value = getTemplateListItemById(id) || template || null;
  6272. }
  6273. return modalTemplateDetail.value;
  6274. } catch (error) {
  6275. if (requestToken === templatePreviewRequestToken) {
  6276. modalTemplateDetail.value = null;
  6277. }
  6278. return null;
  6279. } finally {
  6280. if (requestToken === templatePreviewRequestToken) {
  6281. templatePreviewLoading.value = false;
  6282. }
  6283. }
  6284. };
  6285. const handleTemplateItemClick = async (template: TemplateListItem) => {
  6286. if (activeTemplateTab.value === 'plugin' || Number(template.id) < 0) {
  6287. syncModalTemplatePreviewState(template);
  6288. return;
  6289. }
  6290. await loadTemplatePreviewDetail(template);
  6291. };
  6292. const handleTemplateItemConfirm = async (template: TemplateListItem) => {
  6293. await handleTemplateItemClick(template);
  6294. await confirmTemplateSelect();
  6295. };
  6296. const closeTemplateModal = () => {
  6297. templateModalOpen.value = false;
  6298. templatePreviewRequestToken += 1;
  6299. templatePreviewLoading.value = false;
  6300. modalTemplateDetail.value = null;
  6301. modalTemplateItem.value = null;
  6302. };
  6303. const fetchAllTemplates = async () => {
  6304. // 模板选择弹窗没有分页交互,打开时直接单次拉取较大的 limit,避免多次连续请求。
  6305. const limit = 9999;
  6306. const resp = await tplPage({ page: 1, limit } as any);
  6307. const list = ((resp as any)?.list || (resp as any)?.records || []) as TemplateListItem[];
  6308. const result = Array.isArray(list) ? list : [];
  6309. const uniqueMap = new Map<string, TemplateListItem>();
  6310. result.forEach((item, index) => {
  6311. uniqueMap.set(String(item.id ?? `template_${index}`), item);
  6312. });
  6313. return Array.from(uniqueMap.values());
  6314. };
  6315. const loadTemplateList = async (options: { silent?: boolean; preferredId?: number } = {}) => {
  6316. templateListLoading.value = true;
  6317. try {
  6318. const list = await fetchAllTemplates();
  6319. templateList.value = mergeTemplateCollections(mockTemplateList, list);
  6320. templateListLoaded.value = templateList.value.length > 0;
  6321. if (!templateList.value.length && !options.silent) {
  6322. createMessage.info('模板列表暂无可用数据');
  6323. }
  6324. } catch (error) {
  6325. templateList.value = mergeTemplateCollections(mockTemplateList, templateList.value);
  6326. templateListLoaded.value = templateList.value.length > 0;
  6327. if (!options.silent && !templateList.value.length) {
  6328. createMessage.warning('模板列表接口暂不可用');
  6329. }
  6330. } finally {
  6331. templateListLoading.value = false;
  6332. }
  6333. syncActiveTemplateItem(options.preferredId ?? selectedTemplateId.value);
  6334. if (selectedTemplateId.value) {
  6335. syncSelectedTemplateState(selectedTemplateId.value);
  6336. }
  6337. };
  6338. const syncSelectedTemplateState = (templateId?: string | number | null) => {
  6339. const id = normalizeTemplateId(templateId ?? selectedTemplateId.value);
  6340. selectedTemplateId.value = id;
  6341. if (!id) {
  6342. templateDetail.value = null;
  6343. templateDetailFromMock.value = false;
  6344. activeTemplateItem.value = null;
  6345. return { id, matchedListItem: null, cachedDetail: null };
  6346. }
  6347. const matchedListItem = getTemplateListItemById(id);
  6348. const cachedDetail = templateDetailCache.get(id) || null;
  6349. templateDetail.value = cachedDetail
  6350. ? mergeTemplateInfo(matchedListItem, cachedDetail)
  6351. : matchedListItem || mockTemplateMap[id] || null;
  6352. templateDetailFromMock.value = !cachedDetail && !matchedListItem && !!mockTemplateMap[id];
  6353. if (templateDetail.value) {
  6354. mergeTemplateListItem(templateDetail.value);
  6355. }
  6356. activeTemplateItem.value =
  6357. getTemplateListItemById(id) || matchedListItem || (templateDetail.value as TemplateListItem | null) || null;
  6358. return { id, matchedListItem, cachedDetail };
  6359. };
  6360. const loadTemplateDetail = async (
  6361. templateId?: string | number | null,
  6362. options: { silent?: boolean; requestDetail?: boolean } = {},
  6363. ) => {
  6364. const id = normalizeTemplateId(templateId ?? selectedTemplateId.value);
  6365. if (!id) {
  6366. syncSelectedTemplateState(id);
  6367. return templateDetail.value;
  6368. }
  6369. if (!templateListLoaded.value && !templateListLoading.value) {
  6370. await loadTemplateList({ silent: true, preferredId: id });
  6371. }
  6372. const { matchedListItem, cachedDetail } = syncSelectedTemplateState(id);
  6373. if (!id || !options.requestDetail || cachedDetail) {
  6374. return templateDetail.value;
  6375. }
  6376. try {
  6377. const resp = await tplDetail({ id });
  6378. const detail = resp || null;
  6379. if (detail) {
  6380. templateDetailCache.set(id, detail);
  6381. }
  6382. templateDetail.value = detail
  6383. ? mergeTemplateInfo(matchedListItem, detail)
  6384. : matchedListItem || mockTemplateMap[id] || null;
  6385. templateDetailFromMock.value = !detail && !matchedListItem && !!mockTemplateMap[id];
  6386. if (templateDetail.value) {
  6387. mergeTemplateListItem(templateDetail.value);
  6388. activeTemplateItem.value = getTemplateListItemById(id) || (templateDetail.value as TemplateListItem);
  6389. }
  6390. } catch (error) {
  6391. templateDetail.value = matchedListItem || mockTemplateMap[id] || null;
  6392. templateDetailFromMock.value = !matchedListItem && !!mockTemplateMap[id];
  6393. activeTemplateItem.value = (matchedListItem || (templateDetail.value as TemplateListItem | null)) ?? null;
  6394. if (!options.silent) {
  6395. createMessage.warning('模板详情接口暂不可用,已使用当前列表匹配结果');
  6396. }
  6397. }
  6398. return templateDetail.value;
  6399. };
  6400. const openTemplateModal = async () => {
  6401. templateModalOpen.value = true;
  6402. activeTemplateTab.value = 'template';
  6403. activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
  6404. templateKeyword.value = '';
  6405. templateSource.value = 0;
  6406. await Promise.all([
  6407. loadTemplateCategories(),
  6408. loadTemplateList({ silent: true, preferredId: selectedTemplateId.value }),
  6409. ]);
  6410. const previewTarget = syncModalTemplateItem(selectedTemplateId.value);
  6411. await loadTemplatePreviewDetail(previewTarget, { preferAppliedDetail: true });
  6412. };
  6413. watch([activeCategory, templateSource, templateKeyword, activeTemplateTab], async () => {
  6414. if (!templateModalOpen.value) return;
  6415. if (!showTemplateTabSwitch.value) {
  6416. activeTemplateTab.value = 'template';
  6417. }
  6418. const previewTarget = syncModalTemplateItem(
  6419. activeTemplateTab.value === 'plugin' ? undefined : selectedTemplateId.value,
  6420. );
  6421. await loadTemplatePreviewDetail(previewTarget, {
  6422. preferAppliedDetail:
  6423. activeTemplateTab.value !== 'plugin' && normalizeTemplateId(previewTarget?.id) === selectedTemplateId.value,
  6424. });
  6425. });
  6426. watch(activeTemplateTab, (tab) => {
  6427. if (!templateModalOpen.value) return;
  6428. if (tab === 'plugin') {
  6429. modalTemplateDetail.value = null;
  6430. templatePreviewLoading.value = false;
  6431. templatePreviewRequestToken += 1;
  6432. }
  6433. });
  6434. watch(categories, (nextCategories) => {
  6435. if (!nextCategories.includes(activeCategory.value)) {
  6436. activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
  6437. }
  6438. });
  6439. const modalPreviewTemplate = computed(() => mergeTemplateInfo(modalTemplateItem.value, modalTemplateDetail.value));
  6440. const previewRules = computed(() => {
  6441. const fields = buildTargetFieldsFromTemplateInfo(modalPreviewTemplate.value);
  6442. if (fields.length) {
  6443. return fields.map((item) => ({
  6444. targetField: item.label || '-',
  6445. originalField: item.key || '-',
  6446. required: item.required,
  6447. }));
  6448. }
  6449. return getTemplateHeaderList(modalPreviewTemplate.value).map((item) => ({
  6450. targetField: item,
  6451. originalField: item,
  6452. required: false,
  6453. }));
  6454. });
  6455. const modalTemplateHeaderList = computed(() => getTemplateHeaderList(modalPreviewTemplate.value));
  6456. const modalTemplateSourceLabel = computed(() => getTemplateSourceLabel(modalPreviewTemplate.value));
  6457. const modalTemplateFieldCount = computed(() => previewRules.value.length);
  6458. const activeNodeInfo = computed(() => {
  6459. return activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
  6460. });
  6461. const activeNodeTemplateMatchedFail = computed(() => isTemplateMatchedFailStatus(activeNodeInfo.value?.fileStatus));
  6462. watch(
  6463. [
  6464. selectedTemplateId,
  6465. templateDetail,
  6466. activeTemplateItem,
  6467. fieldMapping,
  6468. rightTableHeaders,
  6469. addedColumns,
  6470. selectRules,
  6471. selectedRules,
  6472. headerRowIndex,
  6473. addedColumnConfig,
  6474. splitColumnConfig,
  6475. recognitionDirectionFields,
  6476. recognitionBasicInfoBaseFuncRegx,
  6477. recognitionBasicInfoManualVisible,
  6478. recognitionBasicInfoRows,
  6479. recognitionAmountZfEnabled,
  6480. childTemplateName,
  6481. ],
  6482. () => {
  6483. syncActiveNodeStructure();
  6484. },
  6485. { deep: true },
  6486. );
  6487. const hasTemplateSelected = computed(() => {
  6488. const activeTemplateId = normalizeTemplateId(
  6489. templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  6490. );
  6491. if (activeTemplateId > 0) return true;
  6492. if (activeNodeTemplateMatchedFail.value) return false;
  6493. return (
  6494. normalizeTemplateId(activeNodeInfo.value?.templateId) > 0 ||
  6495. normalizeTableRuleList(activeNodeInfo.value?.tableRuleList).length > 0 ||
  6496. !!normalizeRuleText(activeNodeInfo.value?.tableName)
  6497. );
  6498. });
  6499. const activeSheetDisplayName = computed(() => {
  6500. return activeNodeInfo.value?.sheetName || activeNodeInfo.value?.fileName || '当前工作表';
  6501. });
  6502. const currentTemplateName = computed(() => {
  6503. return (
  6504. templateDetail.value?.tableNameCn ||
  6505. activeTemplateItem.value?.tableNameCn ||
  6506. activeNodeInfo.value?.tableName ||
  6507. '-'
  6508. );
  6509. });
  6510. const currentTemplateDisplayName = computed(() => {
  6511. if (!hasTemplateSelected.value) return '未选择模板';
  6512. return currentTemplateName.value === '-' ? '已选择模板' : currentTemplateName.value;
  6513. });
  6514. const showRecognitionAmountZfPanel = computed(
  6515. () => recognitionAmountZfEnabled.value || fixedTargetFields.value.some((item) => isTransAmountTargetField(item)),
  6516. );
  6517. const recognitionRuleTabs = computed(() => {
  6518. const tabs: string[] = [];
  6519. if (showRecognitionAmountZfPanel.value) {
  6520. tabs.push('正负标识');
  6521. }
  6522. if (hasRecognitionLoanTab.value) {
  6523. tabs.push('借贷标志');
  6524. }
  6525. if (hasRecognitionCommunicationTab.value) {
  6526. tabs.push('通信类别');
  6527. }
  6528. if (showRecognitionBasicInfoPanel.value) {
  6529. tabs.push('基本信息配置');
  6530. }
  6531. return Array.from(new Set(tabs));
  6532. });
  6533. const showRecognitionPanel = computed(() => recognitionRuleTabs.value.length > 0);
  6534. const activeRecognitionRuleDesc = computed(() => {
  6535. const templateName = currentTemplateName.value === '-' ? '当前模板' : currentTemplateName.value;
  6536. const ruleLabel = activeRecognitionRule.value || recognitionRuleTabs.value[0] || '识别规则';
  6537. return `围绕「${templateName}」预置 ${ruleLabel} 的识别配置,后续可结合导入样本继续沉淀成子模板。`;
  6538. });
  6539. watch(
  6540. recognitionRuleTabs,
  6541. (tabs) => {
  6542. if (!tabs.length) {
  6543. activeRecognitionRule.value = '';
  6544. return;
  6545. }
  6546. if (!tabs.includes(activeRecognitionRule.value)) {
  6547. activeRecognitionRule.value = tabs[0];
  6548. }
  6549. },
  6550. { immediate: true },
  6551. );
  6552. watch(
  6553. fixedTargetFields,
  6554. (fields) => {
  6555. if (activeMappingTargetKey.value && !fields.some((item) => item.key === activeMappingTargetKey.value)) {
  6556. activeMappingTargetKey.value = '';
  6557. }
  6558. },
  6559. { deep: true },
  6560. );
  6561. const confirmTemplateSelect = async () => {
  6562. if (!modalTemplateItem.value?.id) return;
  6563. if (Number(modalTemplateItem.value.id) < 0) {
  6564. createMessage.info('模板插件暂未接入详情逻辑');
  6565. closeTemplateModal();
  6566. return;
  6567. }
  6568. const selectedId = normalizeTemplateId(modalTemplateItem.value.id);
  6569. const previousSelectedId = normalizeTemplateId(selectedTemplateId.value);
  6570. const shouldResetTemplateState = !!previousSelectedId && previousSelectedId !== selectedId;
  6571. const sourcePreviewSnapshot = shouldResetTemplateState ? captureSourcePreviewSnapshot() : null;
  6572. if (shouldResetTemplateState) {
  6573. structureSyncSuspended.value = true;
  6574. try {
  6575. clearActiveNodeTemplateState();
  6576. resetNodeWorkState();
  6577. if (sourcePreviewSnapshot) {
  6578. restorePreviewSnapshot(sourcePreviewSnapshot);
  6579. } else {
  6580. await restorePreviewFromNode(activeNodeInfo.value);
  6581. }
  6582. } finally {
  6583. structureSyncSuspended.value = false;
  6584. }
  6585. }
  6586. selectedTemplateId.value = selectedId;
  6587. activeTemplateItem.value = modalTemplateItem.value;
  6588. if (modalTemplateDetail.value && normalizeTemplateId(modalTemplateDetail.value.id) === selectedId) {
  6589. templateDetailCache.set(selectedId, modalTemplateDetail.value);
  6590. }
  6591. closeTemplateModal();
  6592. await loadTemplateDetail(selectedTemplateId.value, { silent: true, requestDetail: true });
  6593. applyRecognitionDirectionState(activeNodeInfo.value, { preferTemplate: true });
  6594. applyRecognitionBasicInfoState(activeNodeInfo.value, { preferTemplate: true });
  6595. applyRecognitionAmountZfState(activeNodeInfo.value, { preferTemplate: true });
  6596. autoMatchFields(false);
  6597. saveCurrentNodeState();
  6598. };
  6599. const startResize = (type: 'left' | 'middle', event: MouseEvent) => {
  6600. event.preventDefault();
  6601. resizeState.value = {
  6602. type,
  6603. startX: event.clientX,
  6604. startLeft: leftPanelWidth.value,
  6605. startMiddle: middlePanelWidth.value,
  6606. };
  6607. document.body.classList.add('is-resizing-layout');
  6608. document.body.style.cursor = 'col-resize';
  6609. };
  6610. const applyResize = () => {
  6611. if (!resizeState.value.type || pendingClientX.value === null) return;
  6612. const deltaX = pendingClientX.value - resizeState.value.startX;
  6613. if (resizeState.value.type === 'left' && !leftCollapsed.value) {
  6614. const next = resizeState.value.startLeft + deltaX;
  6615. leftPanelWidth.value = Math.min(maxLeftWidth, Math.max(minLeftWidth, next));
  6616. }
  6617. if (resizeState.value.type === 'middle') {
  6618. const next = resizeState.value.startMiddle + deltaX;
  6619. middlePanelWidth.value = Math.min(maxMiddleWidth, Math.max(minMiddleWidth, next));
  6620. }
  6621. rafId.value = null;
  6622. };
  6623. const onResizeMouseMove = (event: MouseEvent) => {
  6624. if (!resizeState.value.type) return;
  6625. event.preventDefault();
  6626. pendingClientX.value = event.clientX;
  6627. if (rafId.value === null) {
  6628. rafId.value = window.requestAnimationFrame(applyResize);
  6629. }
  6630. };
  6631. const stopResize = () => {
  6632. resizeState.value.type = '';
  6633. resizeState.value.startX = 0;
  6634. pendingClientX.value = null;
  6635. if (rafId.value !== null) {
  6636. window.cancelAnimationFrame(rafId.value);
  6637. rafId.value = null;
  6638. }
  6639. document.body.classList.remove('is-resizing-layout');
  6640. document.body.style.cursor = '';
  6641. };
  6642. const resetAddColumnForm = () => {
  6643. addColumnDialogOpen.value = false;
  6644. addColumnForm.value = { name: '', mode: 'ref', contentFields: [], customContent: '' };
  6645. selectedSourceField.value = '';
  6646. selectedContentIndex.value = -1;
  6647. draggingContentIndex.value = null;
  6648. dragOverContentIndex.value = null;
  6649. };
  6650. const resetSplitForm = () => {
  6651. splitColumnDialogOpen.value = false;
  6652. splitColumnForm.value = {
  6653. source: '',
  6654. targetName1: '',
  6655. targetName2: '',
  6656. delimiterType: 'length',
  6657. delimiterValue: '',
  6658. };
  6659. };
  6660. const openAddColumn = () => {
  6661. addColumnMode.value = 'add';
  6662. addColumnDialogOpen.value = true;
  6663. addColumnForm.value = { name: '', mode: 'ref', contentFields: [], customContent: '' };
  6664. selectedSourceField.value = availableSourceHeaders.value[0] || '';
  6665. selectedContentIndex.value = -1;
  6666. draggingContentIndex.value = null;
  6667. dragOverContentIndex.value = null;
  6668. };
  6669. const openSplitColumn = () => {
  6670. if (!availableSourceHeaders.value.length) {
  6671. createMessage.warning('当前没有可供拆分的原始列');
  6672. return;
  6673. }
  6674. splitColumnDialogOpen.value = true;
  6675. splitColumnForm.value = {
  6676. source: availableSourceHeaders.value[0] || '',
  6677. targetName1: '',
  6678. targetName2: '',
  6679. delimiterType: 'length',
  6680. delimiterValue: '',
  6681. };
  6682. };
  6683. const openEditColumn = () => {
  6684. if (!selectedAddedColumn.value) {
  6685. createMessage.warning('请先选中一个新增列');
  6686. return;
  6687. }
  6688. addColumnMode.value = 'edit';
  6689. addColumnDialogOpen.value = true;
  6690. const config = addedColumnConfig.value[selectedAddedColumn.value];
  6691. addColumnForm.value = {
  6692. name: selectedAddedColumn.value,
  6693. mode: config?.mode || 'ref',
  6694. contentFields: (config?.contentFields || []).filter((field) => availableSourceHeaders.value.includes(field)),
  6695. customContent: config?.customContent || '',
  6696. };
  6697. selectedSourceField.value = availableSourceHeaders.value[0] || '';
  6698. selectedContentIndex.value = -1;
  6699. draggingContentIndex.value = null;
  6700. dragOverContentIndex.value = null;
  6701. };
  6702. const openDeleteColumn = () => {
  6703. if (!selectedAddedColumn.value) {
  6704. createMessage.warning('请先选中一个新增列');
  6705. return;
  6706. }
  6707. deleteColumnDialogOpen.value = true;
  6708. };
  6709. const appendSourceField = (field: string) => {
  6710. if (addColumnForm.value.mode !== 'ref') return;
  6711. if (!availableSourceHeaders.value.includes(field)) return;
  6712. addColumnForm.value.contentFields.push(field);
  6713. selectedContentIndex.value = addColumnForm.value.contentFields.length - 1;
  6714. };
  6715. const removeContentField = (index: number) => {
  6716. addColumnForm.value.contentFields.splice(index, 1);
  6717. if (selectedContentIndex.value >= addColumnForm.value.contentFields.length) {
  6718. selectedContentIndex.value = addColumnForm.value.contentFields.length - 1;
  6719. }
  6720. if (draggingContentIndex.value !== null && draggingContentIndex.value >= addColumnForm.value.contentFields.length) {
  6721. draggingContentIndex.value = null;
  6722. }
  6723. if (dragOverContentIndex.value !== null && dragOverContentIndex.value >= addColumnForm.value.contentFields.length) {
  6724. dragOverContentIndex.value = null;
  6725. }
  6726. };
  6727. const clearContentDragState = () => {
  6728. draggingContentIndex.value = null;
  6729. dragOverContentIndex.value = null;
  6730. };
  6731. const reorderContentFields = (fromIndex: number, insertIndex: number) => {
  6732. if (fromIndex < 0 || fromIndex >= addColumnForm.value.contentFields.length) return;
  6733. const list = [...addColumnForm.value.contentFields];
  6734. const [field] = list.splice(fromIndex, 1);
  6735. if (field === undefined) return;
  6736. const nextIndex = Math.min(Math.max(insertIndex - (fromIndex < insertIndex ? 1 : 0), 0), list.length);
  6737. list.splice(nextIndex, 0, field);
  6738. addColumnForm.value.contentFields = list;
  6739. selectedContentIndex.value = nextIndex;
  6740. draggingContentIndex.value = nextIndex;
  6741. dragOverContentIndex.value = nextIndex;
  6742. };
  6743. const resolveContentDropInsertIndex = (index: number, event: DragEvent) => {
  6744. const currentTarget = event.currentTarget as HTMLElement | null;
  6745. if (!currentTarget) return index;
  6746. const rect = currentTarget.getBoundingClientRect();
  6747. return event.clientY > rect.top + rect.height / 2 ? index + 1 : index;
  6748. };
  6749. const handleContentDragStart = (index: number, event: DragEvent) => {
  6750. draggingContentIndex.value = index;
  6751. dragOverContentIndex.value = index;
  6752. selectedContentIndex.value = index;
  6753. if (event.dataTransfer) {
  6754. event.dataTransfer.effectAllowed = 'move';
  6755. event.dataTransfer.setData('text/plain', String(index));
  6756. }
  6757. };
  6758. const handleContentDragEnter = (index: number) => {
  6759. if (draggingContentIndex.value === null) return;
  6760. dragOverContentIndex.value = index;
  6761. };
  6762. const handleContentDragOver = (index: number, event: DragEvent) => {
  6763. if (draggingContentIndex.value === null) return;
  6764. event.preventDefault();
  6765. dragOverContentIndex.value = index;
  6766. if (event.dataTransfer) {
  6767. event.dataTransfer.dropEffect = 'move';
  6768. }
  6769. };
  6770. const handleContentDrop = (index: number, event: DragEvent) => {
  6771. if (draggingContentIndex.value === null) return;
  6772. event.preventDefault();
  6773. const insertIndex = resolveContentDropInsertIndex(index, event);
  6774. reorderContentFields(draggingContentIndex.value, insertIndex);
  6775. clearContentDragState();
  6776. };
  6777. const handleContentDragEnd = () => {
  6778. clearContentDragState();
  6779. };
  6780. const getEnabledGroupingColumnsForHeader = (header: string) => {
  6781. if (!isCleaningRuleEnabledInUi('groupingColumns')) {
  6782. return [];
  6783. }
  6784. const columnIndex = rightTableHeaders.value.findIndex((item) => item === header);
  6785. if (columnIndex < 0) return [];
  6786. const groupingRule = [...(selectRules.value[columnIndex] || [])]
  6787. .reverse()
  6788. .find((rule) => rule.enabled && rule.key === 'groupingColumns');
  6789. if (!groupingRule) return [];
  6790. return normalizeGroupingColumnsDraft(groupingRule.rules?.groupColumns).filter(
  6791. (item) => !!item.header && !!item.direction && Number(item.count ?? 0) > 0,
  6792. );
  6793. };
  6794. const buildEnabledGroupingColumnsPayload = () =>
  6795. rightTableHeaders.value.reduce(
  6796. (acc, header) => {
  6797. if (!addedColumns.value.includes(header)) {
  6798. return acc;
  6799. }
  6800. const groupColumns = getEnabledGroupingColumnsForHeader(header);
  6801. if (!groupColumns.length) {
  6802. return acc;
  6803. }
  6804. acc.push({
  6805. targetHeader: header,
  6806. groupFlag: true,
  6807. groupColumns: cloneDeepSafe(groupColumns),
  6808. });
  6809. return acc;
  6810. },
  6811. [] as Array<{
  6812. targetHeader: string;
  6813. groupFlag: boolean;
  6814. groupColumns: Array<Record<string, any>>;
  6815. }>,
  6816. );
  6817. const setGeneratedColumnCell = (row: Record<string, string>, rowIndex: number, columnName: string, value: string) => {
  6818. row[columnName] = getGeneratedColumnHeaderRowIndex(columnName) === rowIndex ? columnName : value;
  6819. };
  6820. const rebuildGeneratedColumnsInSourceRows = () => {
  6821. if (!addedColumns.value.length || !sourceTableRows.value.length) return;
  6822. const addedHeaderSet = new Set(addedColumns.value);
  6823. sourceTableRows.value = sourceTableRows.value.map((row, rowIndex) => {
  6824. const nextRow = { ...row };
  6825. const handledSplitColumns = new Set<string>();
  6826. rightTableHeaders.value.forEach((header) => {
  6827. if (!addedHeaderSet.has(header)) return;
  6828. const groupingColumns = getEnabledGroupingColumnsForHeader(header);
  6829. if (groupingColumns.length) {
  6830. setGeneratedColumnCell(
  6831. nextRow,
  6832. rowIndex,
  6833. header,
  6834. String(groupColumnsFnValue(nextRow, groupingColumns) ?? ''),
  6835. );
  6836. return;
  6837. }
  6838. const splitConfig = splitColumnConfig.value[header];
  6839. if (splitConfig) {
  6840. const splitKey = [
  6841. splitConfig.source,
  6842. splitConfig.targetName1,
  6843. splitConfig.targetName2,
  6844. splitConfig.delimiterType,
  6845. splitConfig.delimiterValue,
  6846. ].join('::');
  6847. if (handledSplitColumns.has(splitKey)) return;
  6848. handledSplitColumns.add(splitKey);
  6849. const sourceValue = String(nextRow[splitConfig.source] ?? '');
  6850. const splitResult = splitSourceValueByConfig(
  6851. sourceValue,
  6852. splitConfig.delimiterType,
  6853. splitConfig.delimiterValue,
  6854. );
  6855. if (splitConfig.targetName1 && addedHeaderSet.has(splitConfig.targetName1)) {
  6856. setGeneratedColumnCell(nextRow, rowIndex, splitConfig.targetName1, splitResult.first);
  6857. }
  6858. if (splitConfig.targetName2 && addedHeaderSet.has(splitConfig.targetName2)) {
  6859. setGeneratedColumnCell(nextRow, rowIndex, splitConfig.targetName2, splitResult.second);
  6860. }
  6861. return;
  6862. }
  6863. const addedConfig = addedColumnConfig.value[header];
  6864. if (!addedConfig) return;
  6865. const nextValue =
  6866. addedConfig.mode === 'custom'
  6867. ? addedConfig.customContent || ''
  6868. : addedConfig.contentFields.map((field) => String(nextRow[field] ?? '')).join('');
  6869. setGeneratedColumnCell(nextRow, rowIndex, header, nextValue);
  6870. });
  6871. return nextRow;
  6872. });
  6873. };
  6874. const confirmAddOrEditColumn = () => {
  6875. const name = addColumnForm.value.name.trim();
  6876. const normalizedName = normalizeColumnNameForCompare(name);
  6877. const validContentFields = addColumnForm.value.contentFields.filter((field) =>
  6878. availableSourceHeaders.value.includes(field),
  6879. );
  6880. if (!name) {
  6881. createMessage.warning(addColumnMode.value === 'add' ? '请输入新增列名' : '请输入编辑列名');
  6882. return;
  6883. }
  6884. if (addColumnForm.value.mode === 'ref' && !validContentFields.length) {
  6885. createMessage.warning('请至少选择一个原始数据列');
  6886. return;
  6887. }
  6888. if (addColumnForm.value.mode === 'custom' && !addColumnForm.value.customContent.trim()) {
  6889. createMessage.warning('请输入自定义列内容');
  6890. return;
  6891. }
  6892. const saveConfig = {
  6893. mode: addColumnForm.value.mode,
  6894. contentFields: [...validContentFields],
  6895. customContent: addColumnForm.value.customContent,
  6896. };
  6897. if (addColumnMode.value === 'add') {
  6898. if (hasDuplicateHeaderName(name)) {
  6899. createMessage.warning('列名已存在');
  6900. return;
  6901. }
  6902. rightTableHeaders.value.push(name);
  6903. addedColumns.value.push(name);
  6904. addedColumnConfig.value[name] = saveConfig;
  6905. generatedColumnHeaderRowMap.value[name] = headerRowIndex.value;
  6906. sourceTableRows.value.forEach((row, rowIndex) => {
  6907. if (saveConfig.mode === 'custom') {
  6908. setGeneratedColumnCell(row, rowIndex, name, saveConfig.customContent || '');
  6909. } else {
  6910. setGeneratedColumnCell(
  6911. row,
  6912. rowIndex,
  6913. name,
  6914. saveConfig.contentFields.map((field) => row[field] || '').join(''),
  6915. );
  6916. }
  6917. });
  6918. selectedAddedColumn.value = name;
  6919. currentColumn.value = rightTableHeaders.value.findIndex((h) => h === name);
  6920. createMessage.success('新增列成功');
  6921. } else {
  6922. const oldName = selectedAddedColumn.value;
  6923. if (!oldName) return;
  6924. const preservedHeaderRowIndex = getGeneratedColumnHeaderRowIndex(oldName);
  6925. const normalizedOldName = normalizeColumnNameForCompare(oldName);
  6926. if (normalizedName !== normalizedOldName && hasDuplicateHeaderName(name, [oldName])) {
  6927. createMessage.warning('列名已存在');
  6928. return;
  6929. }
  6930. rightTableHeaders.value = rightTableHeaders.value.map((h) => (h === oldName ? name : h));
  6931. addedColumns.value = addedColumns.value.map((h) => (h === oldName ? name : h));
  6932. Object.keys(fieldMapping.value).forEach((key) => {
  6933. if (fieldMapping.value[key] === oldName) {
  6934. fieldMapping.value[key] = name;
  6935. }
  6936. });
  6937. const headerKeyMap = oldName && name !== oldName ? { [oldName]: name } : {};
  6938. if (Object.keys(headerKeyMap).length) {
  6939. selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, headerKeyMap);
  6940. groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, headerKeyMap);
  6941. }
  6942. if (name !== oldName) {
  6943. delete generatedColumnHeaderRowMap.value[oldName];
  6944. }
  6945. generatedColumnHeaderRowMap.value[name] = preservedHeaderRowIndex;
  6946. sourceTableRows.value.forEach((row, rowIndex) => {
  6947. if (saveConfig.mode === 'custom') {
  6948. setGeneratedColumnCell(row, rowIndex, name, saveConfig.customContent || '');
  6949. } else {
  6950. setGeneratedColumnCell(
  6951. row,
  6952. rowIndex,
  6953. name,
  6954. saveConfig.contentFields.map((field) => row[field] || '').join(''),
  6955. );
  6956. }
  6957. if (name !== oldName) {
  6958. delete row[oldName];
  6959. }
  6960. });
  6961. delete addedColumnConfig.value[oldName];
  6962. addedColumnConfig.value[name] = saveConfig;
  6963. selectedAddedColumn.value = name;
  6964. currentColumn.value = rightTableHeaders.value.findIndex((h) => h === name);
  6965. createMessage.success('编辑列成功');
  6966. }
  6967. runRulePipeline();
  6968. resetAddColumnForm();
  6969. };
  6970. const confirmSplitColumn = () => {
  6971. const source = splitColumnForm.value.source;
  6972. const target1 = splitColumnForm.value.targetName1.trim();
  6973. const target2 = splitColumnForm.value.targetName2.trim();
  6974. const normalizedTarget1 = normalizeColumnNameForCompare(target1);
  6975. const normalizedTarget2 = normalizeColumnNameForCompare(target2);
  6976. const delimiterType = normalizeSplitColumnMode(splitColumnForm.value.delimiterType);
  6977. const delimiterValue = normalizeSplitColumnValue(
  6978. splitColumnForm.value.delimiterType,
  6979. splitColumnForm.value.delimiterValue,
  6980. );
  6981. const splitLength = resolveSplitColumnLength(
  6982. splitColumnForm.value.delimiterType,
  6983. splitColumnForm.value.delimiterValue,
  6984. );
  6985. if (!source) {
  6986. createMessage.warning('请选择需要拆分的列');
  6987. return;
  6988. }
  6989. if (!availableSourceHeaders.value.includes(source)) {
  6990. createMessage.warning('拆分来源只能选择原始列');
  6991. return;
  6992. }
  6993. if (!target1) {
  6994. createMessage.warning('请输入拆分后列名1');
  6995. return;
  6996. }
  6997. if (!target2) {
  6998. createMessage.warning('请输入拆分后列名2');
  6999. return;
  7000. }
  7001. if (normalizedTarget1 === normalizedTarget2) {
  7002. createMessage.warning('拆分后两个列名不能相同');
  7003. return;
  7004. }
  7005. if (hasDuplicateHeaderName(target1)) {
  7006. createMessage.warning('拆分后列名1已存在');
  7007. return;
  7008. }
  7009. if (hasDuplicateHeaderName(target2)) {
  7010. createMessage.warning('拆分后列名2已存在');
  7011. return;
  7012. }
  7013. if (delimiterType === 'length' && !splitLength) {
  7014. createMessage.warning('请输入正确的字符长度');
  7015. return;
  7016. }
  7017. if (delimiterType === 'special' && !delimiterValue) {
  7018. createMessage.warning('请输入特殊标识');
  7019. return;
  7020. }
  7021. // 拆分后新列统一追加到最右侧,保持新增列区域集中展示。
  7022. rightTableHeaders.value.push(target1, target2);
  7023. addedColumns.value.push(target1, target2);
  7024. generatedColumnHeaderRowMap.value[target1] = headerRowIndex.value;
  7025. generatedColumnHeaderRowMap.value[target2] = headerRowIndex.value;
  7026. sourceTableRows.value.forEach((row, rowIndex) => {
  7027. const sourceVal = String(row[source] || '');
  7028. const splitResult = splitSourceValueByConfig(sourceVal, delimiterType, delimiterValue);
  7029. setGeneratedColumnCell(row, rowIndex, target1, splitResult.first);
  7030. setGeneratedColumnCell(row, rowIndex, target2, splitResult.second);
  7031. });
  7032. // 记录拆分配置
  7033. const config = {
  7034. source,
  7035. delimiterType,
  7036. delimiterValue,
  7037. targetName1: target1,
  7038. targetName2: target2,
  7039. };
  7040. splitColumnConfig.value[target1] = config;
  7041. splitColumnConfig.value[target2] = config;
  7042. selectedAddedColumn.value = target1;
  7043. currentColumn.value = rightTableHeaders.value.findIndex((h) => h === target1);
  7044. runRulePipeline();
  7045. createMessage.success('拆分列成功');
  7046. resetSplitForm();
  7047. };
  7048. const confirmDeleteColumn = () => {
  7049. const name = selectedAddedColumn.value;
  7050. if (!name) return;
  7051. rightTableHeaders.value = rightTableHeaders.value.filter((h) => h !== name);
  7052. addedColumns.value = addedColumns.value.filter((h) => h !== name);
  7053. delete addedColumnConfig.value[name];
  7054. const deletedSplitConfig = splitColumnConfig.value[name];
  7055. delete splitColumnConfig.value[name];
  7056. if (deletedSplitConfig) {
  7057. [deletedSplitConfig.targetName1, deletedSplitConfig.targetName2].filter(Boolean).forEach((targetName) => {
  7058. const targetConfig = splitColumnConfig.value[targetName];
  7059. if (targetName === name || !targetConfig) return;
  7060. splitColumnConfig.value[targetName] = {
  7061. ...targetConfig,
  7062. targetName1: targetConfig.targetName1 === name ? '' : targetConfig.targetName1,
  7063. targetName2: targetConfig.targetName2 === name ? '' : targetConfig.targetName2,
  7064. };
  7065. });
  7066. }
  7067. delete generatedColumnHeaderRowMap.value[name];
  7068. sourceTableRows.value.forEach((row) => {
  7069. delete row[name];
  7070. });
  7071. Object.keys(fieldMapping.value).forEach((key) => {
  7072. if (fieldMapping.value[key] === name) {
  7073. fieldMapping.value[key] = '';
  7074. }
  7075. });
  7076. selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, {}, new Set([name]));
  7077. groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, {}, new Set([name]));
  7078. selectedAddedColumn.value = addedColumns.value[0] || '';
  7079. currentColumn.value = selectedAddedColumn.value
  7080. ? rightTableHeaders.value.findIndex((h) => h === selectedAddedColumn.value)
  7081. : null;
  7082. selectedRowIndex.value = null;
  7083. runRulePipeline();
  7084. deleteColumnDialogOpen.value = false;
  7085. createMessage.success('删除列成功');
  7086. };
  7087. const onClickHeader = (header: string) => {
  7088. const index = rightTableHeaders.value.findIndex((h) => h === header);
  7089. if (index < 0) return;
  7090. closeMappingPopoverPreview();
  7091. focusPreviewColumn(header, index, { resetSelectedRow: true });
  7092. };
  7093. const onClickCell = (rowIndex: number, header: string, colIndex: number) => {
  7094. closeMappingPopoverPreview();
  7095. selectedRowIndex.value = rowIndex;
  7096. focusPreviewColumn(header, colIndex, { resetSelectedRow: false });
  7097. };
  7098. const runRulePipeline = () => {
  7099. rebuildGeneratedColumnsInSourceRows();
  7100. const previewRows = runCleaningPreviewPipeline({
  7101. rows: sourceTableRows.value,
  7102. headers: rightTableHeaders.value,
  7103. columnRules: selectRules.value.map((rules) =>
  7104. rules.filter((rule) => rule.enabled && isCleaningRuleEnabledInUi(rule.key)),
  7105. ),
  7106. ignoredRows: ignoredRowsSet.value,
  7107. headerRowIndex: headerRowIndex.value,
  7108. });
  7109. tablePreviewRows.value = normalizePreviewRowsByTargetFormat(previewRows);
  7110. };
  7111. const rulesListCheck = (index: number) => {
  7112. activeRules.value = index;
  7113. cleaningRulePanelExpanded.value = true;
  7114. currentRuleTitle.value = '';
  7115. cleaningRulesList.value = cleaningRulesList.value.map((item, i) => {
  7116. if (index === 0) {
  7117. return { ...item, show: i !== 0, active: false };
  7118. }
  7119. return { ...item, show: i === 0, active: false };
  7120. });
  7121. };
  7122. const rulesListClick = (key: CleaningRuleKey) => {
  7123. cleaningRulePanelExpanded.value = true;
  7124. cleaningRulesList.value = cleaningRulesList.value.map((item) => ({
  7125. ...item,
  7126. active: item.key === key,
  7127. }));
  7128. currentRuleTitle.value = cleaningRulesList.value.find((item) => item.key === key)?.name || '';
  7129. if (isSharedRuleMode.value) {
  7130. selectedRules.value = selectedRules.value.map((item) => ({ ...item, active: false }));
  7131. return;
  7132. }
  7133. if (currentColumn.value !== null) {
  7134. const list = selectRules.value[currentColumn.value] || [];
  7135. selectRules.value[currentColumn.value] = list.map((item) => ({ ...item, active: false }));
  7136. }
  7137. };
  7138. const rulesListRight = () => {
  7139. const activeRule = cleaningRulesList.value.find((item) => item.active);
  7140. if (!activeRule) {
  7141. createMessage.warning('请先选择一个清洗规则');
  7142. return;
  7143. }
  7144. if (isSharedRuleMode.value && activeRule.key === 'ignoreLine') {
  7145. selectedRules.value = [
  7146. {
  7147. name: activeRule.name,
  7148. key: activeRule.key,
  7149. active: true,
  7150. enabled: false,
  7151. rules: defaultRuleConfig(),
  7152. },
  7153. ];
  7154. omitLineInput.value = String(selectedRules.value[0].rules.omitLine || '');
  7155. currentRuleTitle.value = activeRule.name;
  7156. return;
  7157. }
  7158. if (currentColumn.value === null) {
  7159. createMessage.warning('请先选中一列');
  7160. return;
  7161. }
  7162. if (
  7163. activeRule.key === 'groupingColumns' &&
  7164. !addedColumns.value.includes(rightTableHeaders.value[currentColumn.value])
  7165. ) {
  7166. createMessage.warning('组合列只能配置在新增列上');
  7167. return;
  7168. }
  7169. if (!selectRules.value[currentColumn.value]) {
  7170. selectRules.value[currentColumn.value] = [];
  7171. }
  7172. selectRules.value[currentColumn.value].push({
  7173. name: activeRule.name,
  7174. key: activeRule.key,
  7175. active: false,
  7176. enabled: false,
  7177. rules: defaultRuleConfig(),
  7178. });
  7179. const lastIndex = selectRules.value[currentColumn.value].length - 1;
  7180. selectRulesClick(lastIndex);
  7181. runRulePipeline();
  7182. };
  7183. const dbRulesListClick = (key: CleaningRuleKey) => {
  7184. rulesListClick(key);
  7185. rulesListRight();
  7186. };
  7187. const hydrateRuleFormByActiveRule = (rule?: SelectedRuleItem) => {
  7188. if (!rule) return;
  7189. if (rule.key === 'ignoreLine') {
  7190. omitLineInput.value = String(rule.rules?.omitLine || '');
  7191. }
  7192. if (rule.key === 'dateConfig') {
  7193. dateConfigType.value = Number(rule.rules?.dateType ?? 1) || 1;
  7194. }
  7195. if (rule.key === 'extractData') {
  7196. extractDataBefore.value = String(rule.rules?.beforeModel || '');
  7197. extractDataAfter.value = String(rule.rules?.nextModel || '');
  7198. syncExtractDataMode();
  7199. }
  7200. if (rule.key === 'extractSegment') {
  7201. extractSegmentValue.value = String(rule.rules?.segment || '');
  7202. extractSegmentRow.value = Number(rule.rules?.segmentRow ?? 1) || 1;
  7203. }
  7204. if (rule.key === 'absoluteValue') {
  7205. absoluteValueChecked.value = toRuleBoolean(rule.rules?.absoluteData, false);
  7206. }
  7207. if (rule.key === 'decimalConversion') {
  7208. decimalEnabled.value = toRuleBoolean(rule.rules?.decimalData, false);
  7209. decimalSourceBase.value = normalizeDecimalBaseLabel(rule.rules?.decimalModel2 || '10进制') || '10进制';
  7210. decimalTargetBase.value = normalizeDecimalBaseLabel(rule.rules?.decimalModel || '16进制') || '16进制';
  7211. }
  7212. if (rule.key === 'replaceValue') {
  7213. replaceEnabled.value = toRuleBoolean(rule.rules?.replaceFlag, true);
  7214. replaceMode.value = Number(rule.rules?.replaceModel ?? 0) === 1 ? 1 : 0;
  7215. replaceSource.value = String(rule.rules?.sourceData || '');
  7216. replaceTarget.value = String(rule.rules?.targetData || '');
  7217. }
  7218. if (rule.key === 'removeSpecialCharacters') {
  7219. const specialList: string[] = rule.rules?.specialList || [];
  7220. removeSpaceChecked.value = specialList.includes('移除空格字符');
  7221. removeTabChecked.value = specialList.includes('移除换行字符(tab)');
  7222. removeCustomChecked.value = specialList.includes('要移除的特殊字符');
  7223. removeCustomText.value = String(rule.rules?.specialStr || '');
  7224. }
  7225. if (rule.key === 'groupingColumns') {
  7226. groupingEnabled.value = toRuleBoolean(rule.rules?.groupFlag, false);
  7227. groupingColumnsDraft.value = normalizeGroupingColumnsDraft(rule.rules?.groupColumns);
  7228. }
  7229. if (rule.key === 'magnificationAdjustment') {
  7230. magnificationValue.value = Number(rule.rules?.magnificationAdjustmentValue ?? 1) || 1;
  7231. }
  7232. };
  7233. const selectRulesClick = (index: number) => {
  7234. if (isSharedRuleMode.value) {
  7235. selectedRules.value = selectedRules.value.map((item, idx) => ({
  7236. ...item,
  7237. active: idx === index,
  7238. }));
  7239. const activeRule = selectedRules.value[index];
  7240. currentRuleTitle.value = activeRule?.name || '';
  7241. hydrateRuleFormByActiveRule(activeRule);
  7242. return;
  7243. }
  7244. if (currentColumn.value === null) return;
  7245. const list = selectRules.value[currentColumn.value] || [];
  7246. selectRules.value[currentColumn.value] = list.map((item, idx) => ({
  7247. ...item,
  7248. active: idx === index,
  7249. }));
  7250. const activeRule = selectRules.value[currentColumn.value][index];
  7251. currentRuleTitle.value = activeRule?.name || '';
  7252. hydrateRuleFormByActiveRule(activeRule);
  7253. };
  7254. const rulesListLeft = () => {
  7255. if (isSharedRuleMode.value) {
  7256. const activeIndex = selectedRules.value.findIndex((item) => item.active);
  7257. if (activeIndex < 0) {
  7258. createMessage.warning('请先选择一个已选规则');
  7259. return;
  7260. }
  7261. selectedRules.value.splice(activeIndex, 1);
  7262. selectedRules.value = [...selectedRules.value];
  7263. omitLineInput.value = '';
  7264. currentRuleTitle.value = '';
  7265. runRulePipeline();
  7266. return;
  7267. }
  7268. if (currentColumn.value === null) {
  7269. createMessage.warning('请先选中一列');
  7270. return;
  7271. }
  7272. const list = selectRules.value[currentColumn.value] || [];
  7273. const activeIndex = list.findIndex((item) => item.active);
  7274. if (activeIndex < 0) {
  7275. createMessage.warning('请先选择一个已选规则');
  7276. return;
  7277. }
  7278. list.splice(activeIndex, 1);
  7279. selectRules.value[currentColumn.value] = [...list];
  7280. currentRuleTitle.value = '';
  7281. runRulePipeline();
  7282. };
  7283. const selectRulesDel = () => {
  7284. if (isSharedRuleMode.value) {
  7285. selectedRules.value = [];
  7286. omitLineInput.value = '';
  7287. currentRuleTitle.value = '';
  7288. runRulePipeline();
  7289. return;
  7290. }
  7291. if (currentColumn.value === null) {
  7292. createMessage.warning('请先选中一列');
  7293. return;
  7294. }
  7295. selectRules.value[currentColumn.value] = [];
  7296. currentRuleTitle.value = '';
  7297. runRulePipeline();
  7298. };
  7299. const getCurrentActiveRule = (): SelectedRuleItem | undefined => {
  7300. if (isSharedRuleMode.value) {
  7301. return selectedRules.value.find((rule) => rule.active);
  7302. }
  7303. if (currentColumn.value === null) return undefined;
  7304. return (selectRules.value[currentColumn.value] || []).find((rule) => rule.active);
  7305. };
  7306. const refreshActiveRuleCollection = () => {
  7307. if (isSharedRuleMode.value) {
  7308. selectedRules.value = [...selectedRules.value];
  7309. return;
  7310. }
  7311. selectRules.value = [...selectRules.value];
  7312. };
  7313. const updateCurrentRuleConfig = (
  7314. expectedKey: CleaningRuleKey,
  7315. warningText: string,
  7316. successText: string,
  7317. updater: (rule: SelectedRuleItem) => void,
  7318. ) => {
  7319. const activeRule = getCurrentActiveRule();
  7320. if (!activeRule || activeRule.key !== expectedKey) {
  7321. createMessage.warning(warningText);
  7322. return false;
  7323. }
  7324. updater(activeRule);
  7325. activeRule.enabled = true;
  7326. refreshActiveRuleCollection();
  7327. runRulePipeline();
  7328. createMessage.success(successText);
  7329. return true;
  7330. };
  7331. const toggleCurrentRuleEnabled = (enabled: boolean) => {
  7332. const activeRule = getCurrentActiveRule();
  7333. if (!activeRule) {
  7334. createMessage.warning('请先选择一个已选规则');
  7335. return;
  7336. }
  7337. if (activeRule.enabled === enabled) {
  7338. return;
  7339. }
  7340. activeRule.enabled = enabled;
  7341. refreshActiveRuleCollection();
  7342. runRulePipeline();
  7343. createMessage.success(enabled ? '规则已启用' : '规则已停用');
  7344. };
  7345. const getFilledGroupingFieldCount = (item: GroupingColumnDraft) =>
  7346. [!!item.header, !!item.direction, item.count !== undefined && item.count > 0].filter(Boolean).length;
  7347. const addGroupingColumnDraft = () => {
  7348. groupingColumnsDraft.value = [...groupingColumnsDraft.value, createGroupingColumnDraft()];
  7349. };
  7350. const removeGroupingColumnDraft = (index: number) => {
  7351. const next = groupingColumnsDraft.value.filter((_, itemIndex) => itemIndex !== index);
  7352. groupingColumnsDraft.value = next.length ? next : normalizeGroupingColumnsDraft();
  7353. };
  7354. const handleGroupingColumnHeaderChange = (index: number, value: string) => {
  7355. const row = groupingColumnsDraft.value[index];
  7356. if (!row) return;
  7357. row.header = value;
  7358. row.title = value;
  7359. groupingColumnsDraft.value = [...groupingColumnsDraft.value];
  7360. };
  7361. const applyIgnoreLineRule = () => {
  7362. updateCurrentRuleConfig('ignoreLine', '请先选择忽略行规则', '忽略行规则已应用', (rule) => {
  7363. rule.rules.omitLine = omitLineInput.value || '';
  7364. });
  7365. };
  7366. const applyDateConfigRule = () => {
  7367. updateCurrentRuleConfig('dateConfig', '请先选择日期和时间规则', '日期和时间规则已应用', (rule) => {
  7368. rule.rules.dateType = dateConfigType.value;
  7369. });
  7370. };
  7371. const applyExtractDataRule = () => {
  7372. updateCurrentRuleConfig('extractData', '请先选择提取数据规则', '提取数据规则已应用', (rule) => {
  7373. rule.rules.beforeModel = extractDataMode.value === 'before' ? extractDataBefore.value || '' : '';
  7374. rule.rules.nextModel = extractDataMode.value === 'after' ? extractDataAfter.value || '' : '';
  7375. });
  7376. };
  7377. const applyExtractSegmentRule = () => {
  7378. if (!extractSegmentValue.value.trim()) {
  7379. createMessage.warning('请输入分隔符');
  7380. return;
  7381. }
  7382. updateCurrentRuleConfig('extractSegment', '请先选择分割提取规则', '分割提取规则已应用', (rule) => {
  7383. rule.rules.segment = extractSegmentValue.value || '';
  7384. rule.rules.segmentRow = Number(extractSegmentRow.value ?? 1) || 1;
  7385. });
  7386. };
  7387. const applyTimestampConversionRule = () => {
  7388. updateCurrentRuleConfig('timestampConversion', '请先选择时间戳转换规则', '时间戳转换规则已启用', (rule) => {
  7389. rule.rules.timeStamp = true;
  7390. });
  7391. };
  7392. const applyAbsoluteValueRule = () => {
  7393. updateCurrentRuleConfig('absoluteValue', '请先选择绝对值规则', '绝对值规则已应用', (rule) => {
  7394. rule.rules.absoluteData = absoluteValueChecked.value;
  7395. });
  7396. };
  7397. const applyDecimalConversionRule = () => {
  7398. updateCurrentRuleConfig('decimalConversion', '请先选择进制转换规则', '进制转换规则已应用', (rule) => {
  7399. rule.rules.decimalData = decimalEnabled.value;
  7400. rule.rules.decimalModel = decimalTargetBase.value;
  7401. rule.rules.decimalModel2 = decimalSourceBase.value;
  7402. });
  7403. };
  7404. const applyReplaceRule = () => {
  7405. updateCurrentRuleConfig('replaceValue', '请先选择条件替换规则', '条件替换规则已应用', (rule) => {
  7406. rule.rules.replaceFlag = replaceEnabled.value;
  7407. rule.rules.replaceModel = replaceMode.value;
  7408. rule.rules.sourceData = replaceSource.value || '';
  7409. rule.rules.targetData = replaceTarget.value || '';
  7410. });
  7411. };
  7412. const applyRemoveSpecialRule = () => {
  7413. const specialList: string[] = [];
  7414. if (removeSpaceChecked.value) specialList.push('移除空格字符');
  7415. if (removeTabChecked.value) specialList.push('移除换行字符(tab)');
  7416. if (removeCustomChecked.value) specialList.push('要移除的特殊字符');
  7417. updateCurrentRuleConfig('removeSpecialCharacters', '请先选择移除特殊字符规则', '移除特殊字符规则已应用', (rule) => {
  7418. rule.rules.specialList = specialList;
  7419. rule.rules.specialStr = removeCustomText.value || '';
  7420. });
  7421. };
  7422. const applyGroupingColumnsRule = () => {
  7423. const normalizedRows = groupingColumnsDraft.value.map((item) => createGroupingColumnDraft(item));
  7424. const hasInvalidRow = normalizedRows.some((item) => {
  7425. const filledCount = getFilledGroupingFieldCount(item);
  7426. return filledCount > 0 && filledCount < 3;
  7427. });
  7428. if (hasInvalidRow) {
  7429. createMessage.warning('组合列每一行都需要完整选择表头、方向和位数');
  7430. return;
  7431. }
  7432. const hasValidRow = normalizedRows.some((item) => getFilledGroupingFieldCount(item) === 3);
  7433. if (!hasValidRow) {
  7434. createMessage.warning('请至少配置一条完整的组合列规则');
  7435. return;
  7436. }
  7437. updateCurrentRuleConfig('groupingColumns', '请先选择组合列规则', '组合列规则已应用', (rule) => {
  7438. rule.rules.groupFlag = true;
  7439. rule.rules.groupColumns = normalizedRows;
  7440. });
  7441. };
  7442. const applyMagnificationRule = () => {
  7443. updateCurrentRuleConfig('magnificationAdjustment', '请先选择倍数放大器规则', '倍数放大器规则已应用', (rule) => {
  7444. rule.rules.magnificationAdjustmentValue = Number(magnificationValue.value ?? 1) || 1;
  7445. });
  7446. };
  7447. const applyYearCompleteRule = () => {
  7448. updateCurrentRuleConfig('yearComplete', '请先选择年份补全规则', '年份补全规则已启用', (rule) => {
  7449. rule.rules.yearCompleteEnabled = true;
  7450. });
  7451. };
  7452. const toggleLeft = () => {
  7453. leftCollapsed.value = !leftCollapsed.value;
  7454. };
  7455. const handleSelect = (keys: (string | number)[]) => {
  7456. const rawKey = keys[0] ? String(keys[0]) : '';
  7457. const nextKey = rawKey ? resolveSelectableTreeKey(rawKey) : '';
  7458. selectedKeys.value = nextKey ? [nextKey] : [];
  7459. if (nextKey) {
  7460. switchNodeState(nextKey);
  7461. }
  7462. };
  7463. const handleImportData = async () => {
  7464. saveCurrentNodeState();
  7465. syncActiveNodeStructure();
  7466. const sanitizedFileInfos = sanitizeFileInfoTree(fileList.value, { keepRuleKey: false });
  7467. if (!sanitizedFileInfos.length) {
  7468. createMessage.warning('未找到可导入的数据,请先上传并完成模板匹配');
  7469. return;
  7470. }
  7471. const matchedSheetsWithoutHeaderRow = collectMatchedSheetsWithoutHeaderRow(sanitizedFileInfos);
  7472. if (matchedSheetsWithoutHeaderRow.length) {
  7473. const previewNames = matchedSheetsWithoutHeaderRow.slice(0, 3).join('、');
  7474. const suffix =
  7475. matchedSheetsWithoutHeaderRow.length > 3 ? ` 等 ${matchedSheetsWithoutHeaderRow.length} 个工作表` : '';
  7476. createMessage.warning(`已匹配模板的工作表必须先填写表头所在行:${previewNames}${suffix}`);
  7477. return;
  7478. }
  7479. try {
  7480. const childTemplateNameValid = await ensureChildTemplateNamesAvailable(sanitizedFileInfos);
  7481. if (!childTemplateNameValid) {
  7482. return;
  7483. }
  7484. } catch (error) {
  7485. createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
  7486. return;
  7487. }
  7488. sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizedFileInfos));
  7489. importingData.value = true;
  7490. try {
  7491. const resolveMappedHeaderByTargetField = (item?: TreeFileInfo | null, targetField?: string) => {
  7492. const normalizedTarget = normalizeFieldMatchText(targetField);
  7493. if (!normalizedTarget) return '';
  7494. const matchedRule = normalizeTableRuleList(item?.tableRuleList).find((rule, index) => {
  7495. const targetKey = normalizeFieldMatchText(getTableRuleTargetKey(rule, index));
  7496. const targetLabel = normalizeFieldMatchText(getTableRuleTargetLabel(rule, index));
  7497. return normalizedTarget === targetKey || normalizedTarget === targetLabel;
  7498. });
  7499. return normalizeRecognitionConfigText(matchedRule?.fileColName);
  7500. };
  7501. const buildImportRecognitionBasicInfoFuncRegx = (item?: TreeFileInfo | null) => {
  7502. const source = normalizeRecognitionConfigText(item?.funcRegx);
  7503. if (!source) return '';
  7504. const rows = parseRecognitionBasicInfoRows(source);
  7505. if (!rows.length) return source;
  7506. if (!hasRecognitionBasicInfoFilledContent(rows)) return '';
  7507. const payload = rows.reduce(
  7508. (acc, row) => {
  7509. const targetField = resolveRecognitionBasicInfoTargetField(row.targetField);
  7510. const headerMatch = normalizeRecognitionBasicInfoText(row.headerMatch);
  7511. const endFlag = normalizeRecognitionBasicInfoText(row.endFlag);
  7512. const mappedHeader = resolveMappedHeaderByTargetField(item, targetField);
  7513. const nextKey = headerMatch || mappedHeader || targetField;
  7514. if (!nextKey) return acc;
  7515. acc[nextKey] = [headerMatch, endFlag];
  7516. return acc;
  7517. },
  7518. {} as Record<string, string[]>,
  7519. );
  7520. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  7521. };
  7522. const buildImportFileInfoPayload = (item?: TreeFileInfo | null): TreeFileInfo | null => {
  7523. if (!item) return null;
  7524. const nextItem = cloneDeepSafe(item) as TreeFileInfo;
  7525. const children = Array.isArray(nextItem.children)
  7526. ? nextItem.children
  7527. .map((child) => buildImportFileInfoPayload(child))
  7528. .filter((child): child is TreeFileInfo => Boolean(child))
  7529. : nextItem.children;
  7530. if (isFileRootItem(nextItem)) {
  7531. const rootPayload = {
  7532. ...nextItem,
  7533. children,
  7534. } as TreeFileInfo & { childTemplateName?: string | null };
  7535. delete rootPayload.childTemplateName;
  7536. return rootPayload;
  7537. }
  7538. const childName = normalizeRuleText(nextItem.childTemplateName);
  7539. const hasNew = !!childName || !!nextItem.hasNew;
  7540. const importTemplateId = normalizeTemplateId(
  7541. nextItem.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  7542. );
  7543. const currentTemplate = resolveTemplateInfoById(importTemplateId);
  7544. const parentTemplateId = normalizeTemplateId(nextItem.mainId) || normalizeTemplateId(currentTemplate?.mainId);
  7545. const parentTemplate = (parentTemplateId && resolveTemplateInfoById(parentTemplateId)) || currentTemplate;
  7546. const currentFuncRegx = normalizeRecognitionConfigText(nextItem.funcRegx);
  7547. const importFuncRegx = currentFuncRegx ? buildImportRecognitionBasicInfoFuncRegx(nextItem) : '';
  7548. const sheetPayload = {
  7549. ...nextItem,
  7550. tableName: hasNew ? childName : nextItem.tableName,
  7551. tableNameEn: hasNew
  7552. ? normalizeRuleText(parentTemplate?.tableNameEn) ||
  7553. normalizeRuleText(currentTemplate?.tableNameEn) ||
  7554. normalizeRuleText(nextItem.tableNameEn) ||
  7555. null
  7556. : nextItem.tableNameEn,
  7557. hasNew,
  7558. classify: hasNew
  7559. ? normalizeRuleText(parentTemplate?.classify) ||
  7560. normalizeRuleText(currentTemplate?.classify) ||
  7561. normalizeRuleText(nextItem.classify) ||
  7562. null
  7563. : nextItem.classify,
  7564. funcRegx: importFuncRegx || null,
  7565. children,
  7566. } as TreeFileInfo & { childTemplateName?: string | null };
  7567. return sheetPayload;
  7568. };
  7569. const importFileInfos = sanitizedFileInfos
  7570. .map((item) => buildImportFileInfoPayload(item))
  7571. .filter((item): item is TreeFileInfo => Boolean(item));
  7572. sessionStorage.setItem(IMPORT_FILE_INFO_STORAGE_KEY, JSON.stringify(importFileInfos));
  7573. resetImportProgressSession();
  7574. await go(PageEnum.BASE_CLEAN_PROGRESS);
  7575. } catch {
  7576. createMessage.error('导入数据失败');
  7577. } finally {
  7578. importingData.value = false;
  7579. }
  7580. };
  7581. const loadStoredFileList = () => {
  7582. const raw = sessionStorage.getItem(FILE_INFO_STORAGE_KEY);
  7583. if (!raw) return [];
  7584. try {
  7585. const parsed = JSON.parse(raw) as TreeFileInfo[];
  7586. return Array.isArray(parsed) ? parsed.map((item) => mergeStoredFileInfoWithMock(item)) : [];
  7587. } catch (error) {
  7588. return [];
  7589. }
  7590. };
  7591. const getFocusedNodeKeyFromQuery = () => {
  7592. const rawFocusKey = pageQuery.value?.focusKey;
  7593. return Array.isArray(rawFocusKey) ? normalizeRuleText(rawFocusKey[0]) : normalizeRuleText(rawFocusKey);
  7594. };
  7595. const handleBack = () => {
  7596. // 返回上一级:文件上传页
  7597. go(PageEnum.BASE_UPLOAD);
  7598. };
  7599. const scrollSelectedTreeNodeIntoView = () => {
  7600. void nextTick(() => {
  7601. const selectedNode = document.querySelector(
  7602. '.case-data-cleaning-page .cleaning-file-tree .ant-tree-node-selected',
  7603. );
  7604. const target = (selectedNode?.closest('.ant-tree-treenode') || selectedNode) as HTMLElement | null;
  7605. target?.scrollIntoView({
  7606. block: 'center',
  7607. inline: 'nearest',
  7608. });
  7609. });
  7610. };
  7611. const loadFileTree = async () => {
  7612. const storedList = loadStoredFileList();
  7613. fileList.value = storedList.length ? storedList : sanitizeFileInfoTree(cloneDeepSafe(mockFileList));
  7614. if (!treeData.value.length) return;
  7615. const focusedKey = resolveSelectableTreeKey(getFocusedNodeKeyFromQuery());
  7616. const initialKey =
  7617. focusedKey ||
  7618. (selectedKeys.value.length
  7619. ? resolveSelectableTreeKey(String(selectedKeys.value[0]))
  7620. : findFirstSelectableKey(treeData.value));
  7621. if (!initialKey) return;
  7622. selectedKeys.value = [initialKey];
  7623. await switchNodeState(initialKey);
  7624. if (focusedKey) {
  7625. scrollSelectedTreeNodeIntoView();
  7626. }
  7627. };
  7628. const initializePage = async () => {
  7629. void Promise.all([loadTemplateCategories(), loadTemplateList({ silent: true })]);
  7630. runRulePipeline();
  7631. await loadFileTree();
  7632. };
  7633. onMounted(() => {
  7634. initializePage();
  7635. window.addEventListener('mousemove', onResizeMouseMove);
  7636. window.addEventListener('mouseup', stopResize);
  7637. });
  7638. onBeforeUnmount(() => {
  7639. saveCurrentNodeState();
  7640. window.removeEventListener('mousemove', onResizeMouseMove);
  7641. window.removeEventListener('mouseup', stopResize);
  7642. document.body.classList.remove('is-resizing-layout');
  7643. document.body.style.cursor = '';
  7644. });
  7645. </script>
  7646. <style lang="less">
  7647. // 样式已按域拆分到 ./cleaning/styles/ 下,由此处引入(保持全局,非 scoped)。
  7648. @import './cleaning/styles/index.less';
  7649. </style>