| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214 |
- <template>
- <div :class="prefixCls" class="case-data-cleaning-page">
- <div class="page-header">
- <div class="page-header-main">
- <div class="page-title-area">
- <Button type="text" class="back-btn" @click="handleBack">←</Button>
- <div class="page-title-block">
- <div class="page-title">案件数据清洗</div>
- </div>
- </div>
- <div class="page-header-actions">
- <Button @click="toggleLeft">{{ leftCollapsed ? '展开左侧' : '收起左侧' }}</Button>
- <Button type="primary" :loading="importingData" @click="handleImportData">导入数据</Button>
- </div>
- </div>
- </div>
- <div class="main-layout" :style="{ gridTemplateColumns: layoutTemplateColumns }">
- <aside v-show="!leftCollapsed" class="left-panel">
- <div class="tree-header">
- <span class="label">文件分类</span>
- <div class="actions">
- <Button size="small" @click="sortMode = sortMode === 'asc' ? '' : 'asc'">升序</Button>
- <Button size="small" @click="sortMode = sortMode === 'desc' ? '' : 'desc'">降序</Button>
- </div>
- </div>
- <div class="tree-wrap">
- <BasicTree
- class="cleaning-file-tree"
- :tree-data="treeData"
- :tree-data-simple-mode="false"
- :field-names="{ key: 'key', title: 'title', children: 'children' }"
- :default-expand-all="true"
- v-model:selectedKeys="selectedKeys"
- @select="handleSelect"
- >
- <template #title="node">
- <div v-if="isTreeFileNode(node.raw)" class="tree-file-row" :title="getTreeFileName(node.raw)">
- <span class="tree-file-name">{{ getTreeFileName(node.raw) }}</span>
- </div>
- <div
- v-else
- class="tree-sheet-row"
- :class="{
- 'is-success': getSheetNodeVisualState(node.raw) === 'success',
- 'is-fail': getSheetNodeVisualState(node.raw) === 'fail',
- 'is-analyzing': getSheetNodeVisualState(node.raw) === 'analyzing',
- }"
- :title="getTreeSheetTitle(node.raw)"
- >
- <span class="tree-sheet-status">
- <span
- v-if="getSheetNodeVisualState(node.raw) === 'analyzing'"
- class="tree-sheet-status-spinner"
- ></span>
- <template v-else>
- {{ getSheetNodeVisualState(node.raw) === 'success' ? '✓' : '✕' }}
- </template>
- </span>
- <span class="tree-sheet-name">{{ getTreeSheetName(node.raw) }}</span>
- </div>
- </template>
- </BasicTree>
- </div>
- <div class="legend">
- <div v-for="item in iconTips" :key="item.key" class="legend-item">
- <span class="legend-icon" :class="`is-${item.key}`">
- <span v-if="item.key === 'matched' || item.key === 'unmatched'" class="legend-dot"></span>
- <template v-else>{{ item.icon }}</template>
- </span>
- <span class="legend-text">{{ item.label }}</span>
- </div>
- </div>
- </aside>
- <div v-show="!leftCollapsed" class="panel-resizer" @mousedown="startResize('left', $event)"></div>
- <section class="middle-panel">
- <div class="template-match-bar" :class="{ 'is-empty': !hasTemplateSelected }">
- <div class="template-match-main">
- <span class="template-match-icon"></span>
- <div class="template-match-copy">
- <div class="template-match-label">匹配模板</div>
- <div class="template-match-name">
- {{ currentTemplateDisplayName }}
- <span v-if="!hasTemplateSelected" class="template-match-badge is-pending">待配置</span>
- <span v-else-if="templateDetailFromMock" class="template-match-badge">示例</span>
- </div>
- </div>
- </div>
- <Button type="link" class="template-select-trigger" @click="openTemplateModal">选择模板</Button>
- </div>
- <template v-if="hasTemplateSelected">
- <div class="middle-panel-body">
- <div class="mapping-table-card">
- <div class="mapping-card-head">
- <div class="mapping-card-title">字段映射</div>
- <div class="mapping-card-actions">
- <Button size="small" @click="() => autoMatchFields()">智能预匹配</Button>
- <Button size="small" @click="clearFieldMapping">清空映射</Button>
- </div>
- </div>
- <div class="mapping-table">
- <div class="mapping-head">
- <span>序号</span>
- <span>状态</span>
- <span>目标字段</span>
- <span>原始字段</span>
- </div>
- <div
- class="mapping-row"
- :class="{ 'is-active': activeMappingTargetKey === item.key }"
- v-for="(item, index) in sortedMappingTargetFields"
- :key="item.key"
- @click="handleMappingRowSelect(item.key)"
- >
- <span>{{ index + 1 }}</span>
- <span class="status-cell">
- <span class="mapping-status-group">
- <span class="mapping-status-badge" :class="getMappingStatusClass(item)">
- <span v-if="getMappingStatusClass(item) !== 'is-optional'" class="mapping-status-dot"></span>
- </span>
- <span
- v-if="hasTargetFormatError(item.key)"
- class="mapping-status-error"
- :title="getTargetFormatErrorTitle(item.key) || undefined"
- >
- !
- </span>
- </span>
- </span>
- <span class="target-field-cell">
- <span v-if="isMappingTargetRequired(item)" class="required">*</span>
- {{ item.label }}
- </span>
- <span class="origin-field-cell">
- <Popover
- trigger="click"
- placement="bottomRight"
- overlayClassName="origin-field-popover"
- :open="!isTargetHandledByRecognitionRule(item.key) && mappingPopoverOpenKey === item.key"
- @openChange="(open) => handleMappingPopoverOpen(item.key, open)"
- >
- <template #content>
- <div class="origin-pop">
- <div class="origin-pop-title">
- <span class="small-title">原始字段选择</span>
- <Input
- v-model:value="originFilterKeyword"
- size="small"
- placeholder="请输入搜索的字段名称"
- allow-clear
- />
- </div>
- <div
- class="origin-pop-list"
- :data-origin-list-target-key="item.key"
- @mouseleave="handleOriginOptionHoverLeave(item.key)"
- >
- <div
- class="origin-tag blank"
- @mouseenter="handleOriginOptionHoverLeave(item.key)"
- @click="setFieldBlank(item.key)"
- >
- 置空
- </div>
- <div
- v-for="opt in filteredOriginalOptions"
- :key="opt.value"
- class="origin-tag-wrap"
- @mouseenter="handleOriginOptionHover(item.key, opt.value)"
- >
- <div
- class="origin-tag"
- :data-origin-target-key="item.key"
- :data-origin-option-value="opt.value"
- :class="{
- disabled: isFieldTakenByOther(opt.value, item.key),
- active:
- getDisplayedMappedHeader(item.key) === opt.value ||
- isOriginOptionHovering(item.key, opt.value),
- }"
- @click="selectOriginalField(item.key, opt.value)"
- >
- {{ opt.label }}
- </div>
- <span
- v-if="isFieldTakenByOther(opt.value, item.key)"
- class="replace"
- @click="replaceOriginalField(item.key, opt.value)"
- >替换</span
- >
- </div>
- </div>
- <div class="origin-pop-bottom">+添加自定义表头</div>
- </div>
- </template>
- <div
- class="mapping-field-trigger"
- :class="{ 'is-disabled': isTargetHandledByRecognitionRule(item.key) }"
- @click="handleMappingTriggerClick(item.key)"
- >
- <span
- class="mapping-field-value"
- :class="{
- placeholder:
- !getDisplayedMappedHeader(item.key) && !isTargetHandledByRecognitionRule(item.key),
- }"
- >
- {{
- isTargetHandledByRecognitionRule(item.key)
- ? ''
- : getDisplayedMappedHeader(item.key) || '请选择原始字段'
- }}
- </span>
- <span v-if="!isTargetHandledByRecognitionRule(item.key)" class="mapping-field-arrow">▾</span>
- </div>
- </Popover>
- </span>
- </div>
- <div v-if="!fixedTargetFields.length" class="mapping-empty">当前模板暂无可映射字段</div>
- </div>
- </div>
- <div
- class="template-foot-panel"
- :class="{
- 'is-collapsed': showRecognitionPanel && !recognitionPanelExpanded,
- 'is-compact': !showRecognitionPanel,
- }"
- >
- <div v-if="showRecognitionPanel" class="recognition-panel">
- <div class="recognition-panel-head">
- <span class="recognition-title">识别规则设置</span>
- <span
- class="recognition-toggle"
- :class="{ expanded: recognitionPanelExpanded }"
- @click="recognitionPanelExpanded = !recognitionPanelExpanded"
- >
- {{ recognitionPanelExpanded ? '收起' : '展开' }}
- <component
- :is="recognitionPanelExpanded ? DownOutlined : UpOutlined"
- class="recognition-toggle-icon"
- />
- </span>
- </div>
- <div v-if="recognitionPanelExpanded" class="recognition-panel-body">
- <div class="recognition-tabs">
- <span
- v-for="item in recognitionRuleTabs"
- :key="item"
- class="recognition-tab"
- :class="{ active: activeRecognitionRule === item }"
- @click="activeRecognitionRule = item"
- >
- {{ item }}
- </span>
- </div>
- <div v-if="activeRecognitionRule === '基本信息配置'" class="recognition-basic-shell">
- <div class="recognition-basic-preview">
- <div class="recognition-basic-preview__head">
- <span class="recognition-basic-preview__title">表头上方内容</span>
- <span class="recognition-basic-preview__meta">{{ recognitionHeaderPreviewMeta }}</span>
- </div>
- <div class="recognition-basic-preview__content">{{ recognitionHeaderPreviewText }}</div>
- </div>
- <div v-if="showRecognitionBasicInfoPanel" class="recognition-basic-panel">
- <div class="recognition-basic-panel__head">
- <div class="recognition-basic-panel__summary">
- <div class="recognition-basic-panel__title-wrap">
- <span class="recognition-basic-panel__title">合并表头配置内容</span>
- <span class="recognition-basic-panel__count">{{ recognitionBasicInfoRows.length }} 项</span>
- </div>
- <div class="recognition-basic-panel__desc">
- 跟随模板中的合并表逻辑,为表头上方内容补充原始匹配项、目标字段与截止标志,目标字段保持模板默认映射。
- </div>
- </div>
- <div class="recognition-basic-panel__actions">
- <Button
- type="link"
- class="recognition-basic-panel__toggle-btn"
- @click.stop="recognitionBasicInfoExpanded = !recognitionBasicInfoExpanded"
- >
- {{ recognitionBasicInfoExpanded ? '收起' : '展开' }}
- </Button>
- </div>
- </div>
- <template v-if="recognitionBasicInfoExpanded">
- <div v-if="recognitionBasicInfoRows.length" class="recognition-basic-list">
- <div
- v-for="(item, index) in recognitionBasicInfoRows"
- :key="item.key"
- class="recognition-basic-item"
- >
- <div class="recognition-basic-item__index">{{ String(index + 1).padStart(2, '0') }}</div>
- <div class="recognition-basic-item__content">
- <div class="recognition-basic-field">
- <span class="recognition-basic-field__label">原始表头匹配项</span>
- <Input v-model:value="item.headerMatch" placeholder="请输入匹配项" />
- </div>
- <div class="recognition-basic-field">
- <span class="recognition-basic-field__label">目标字段</span>
- <Tooltip :title="item.targetField || undefined" placement="topLeft">
- <div
- class="recognition-basic-field__readonly"
- :class="{ 'is-empty': !item.targetField }"
- >
- {{ item.targetField || '未配置目标字段' }}
- </div>
- </Tooltip>
- </div>
- <div class="recognition-basic-field">
- <span class="recognition-basic-field__label">内容获取截止标志</span>
- <Input v-model:value="item.endFlag" placeholder="请输入截止标志" />
- </div>
- </div>
- </div>
- </div>
- <div v-else class="recognition-basic-empty">
- <div class="recognition-basic-empty__title">暂无合并配置</div>
- <div class="recognition-basic-empty__desc">
- 当前模板未解析到可展示的合并规则,请检查默认提取规则配置。
- </div>
- </div>
- </template>
- </div>
- <div v-else class="recognition-basic-empty-shell">
- <div class="recognition-basic-empty-shell__title">当前模板暂无合并表配置</div>
- <div class="recognition-basic-empty-shell__desc">
- 当前工作表会沿用模板默认合并表配置;若需调整,请先在模板库中维护对应的默认规则。
- </div>
- </div>
- </div>
- <div v-else-if="activeRecognitionRule === '正负标识'" class="recognition-zf-shell">
- <div class="recognition-zf-card">
- <div class="recognition-zf-card__main">
- <div class="recognition-zf-card__title">交易金额正负标识</div>
- <div class="recognition-zf-card__desc">
- 开启后,系统会依据交易金额的正负值识别收入/支出,借贷标志字段无需再单独映射原始列。
- </div>
- </div>
- <Switch v-model:checked="recognitionAmountZfEnabled" @change="handleRecognitionAmountZfChange" />
- </div>
- </div>
- <div
- v-else-if="activeRecognitionRule === '借贷标志' || activeRecognitionRule === '通信类别'"
- class="recognition-direction-shell"
- >
- <div class="recognition-direction-card-list">
- <div
- v-for="field in activeRecognitionDirectionFields"
- :key="field.key"
- class="recognition-direction-card"
- >
- <div class="recognition-direction-card__head">
- <span class="recognition-direction-card__title">{{ field.label }}</span>
- <span class="recognition-direction-card__tag">{{ field.entries.length }} 项</span>
- </div>
- <div class="recognition-direction-list">
- <div
- v-for="directionItem in field.entries"
- :key="directionItem.key"
- class="recognition-direction-row"
- >
- <span class="recognition-direction-label">{{ directionItem.label }}</span>
- <Tooltip :title="directionItem.value || undefined" placement="topLeft">
- <Input v-model:value="directionItem.value" placeholder="请输入识别关键词" />
- </Tooltip>
- </div>
- </div>
- </div>
- </div>
- <div class="recognition-direction-actions">
- <Button @click="handleClearRecognitionDirection">清空规则</Button>
- <Button type="primary" @click="handleSaveRecognitionDirection">保存</Button>
- </div>
- </div>
- <div v-else class="recognition-rule-placeholder">
- <div class="recognition-rule-placeholder__title">{{ activeRecognitionRule }}</div>
- <div class="recognition-rule-placeholder__desc">
- 当前规则仍沿用已配置的模板字段与右侧清洗逻辑,后续可继续补充专项识别参数。
- </div>
- </div>
- </div>
- </div>
- <div class="child-template-panel">
- <div class="child-template-tip">
- 子模板输入名称,导入数据会自动保存。下次上传同样的数据子模板会自动识别。
- </div>
- <div class="child-template-form">
- <span class="child-template-label">创建子模板:</span>
- <Input
- v-model:value="childTemplateName"
- placeholder="请输入子模板名称"
- @blur="handleChildTemplateNameBlur"
- />
- </div>
- </div>
- </div>
- </div>
- </template>
- <div v-else class="middle-empty-shell">
- <div class="middle-empty-stage">
- <div class="middle-empty-stage-head">
- <span class="middle-empty-sheet">当前工作表:{{ activeSheetDisplayName }}</span>
- </div>
- <div class="middle-empty-stage-main">
- <div class="middle-empty-visual" aria-hidden="true">
- <span class="middle-empty-visual__halo is-large"></span>
- <span class="middle-empty-visual__halo is-small"></span>
- <span class="middle-empty-visual__card"></span>
- <span class="middle-empty-visual__card is-back"></span>
- <span class="middle-empty-visual__dot"></span>
- </div>
- <div class="middle-empty-actions">
- <Button type="primary" @click="openTemplateModal">选择模板</Button>
- <span class="middle-empty-hint">支持从模板库实时加载模板,并同步初始化字段映射区。</span>
- </div>
- </div>
- </div>
- <div class="middle-empty-highlights">
- <div class="middle-empty-card">
- <span class="middle-empty-card__index">01</span>
- <div class="middle-empty-card__title">锁定字段结构</div>
- <div class="middle-empty-card__desc">先明确目标字段与必填项,再进入映射配置,减少重复调整。</div>
- </div>
- <div class="middle-empty-card">
- <span class="middle-empty-card__index">02</span>
- <div class="middle-empty-card__title">自动回填规则</div>
- <div class="middle-empty-card__desc">命中模板后可直接带出字段关系,并用当前表头继续补齐。</div>
- </div>
- <div class="middle-empty-card">
- <span class="middle-empty-card__index">03</span>
- <div class="middle-empty-card__title">沉淀子模板</div>
- <div class="middle-empty-card__desc">完成映射后可保存识别规则,方便同类文件后续自动识别。</div>
- </div>
- </div>
- </div>
- </section>
- <div class="panel-resizer" @mousedown="startResize('middle', $event)"></div>
- <section class="right-panel">
- <div class="panel-header">
- <div class="panel-title-wrap">
- <div class="panel-title">数据清洗区</div>
- </div>
- </div>
- <div class="toolbar toolbar--column-actions">
- <div class="toolbar-head">
- <span class="toolbar-label">列操作</span>
- </div>
- <div class="toolbar-actions">
- <Button size="small" type="primary" @click="openAddColumn">新增列</Button>
- <Button size="small" @click="openSplitColumn">拆分列</Button>
- <Button size="small" @click="openEditColumn">编辑列</Button>
- <Button size="small" @click="openDeleteColumn">删除列</Button>
- </div>
- <div class="toolbar-meta toolbar-meta--header-row">
- <span class="meta-label">表头行</span>
- <Input
- v-model:value="headerRowInput"
- size="small"
- class="meta-input"
- placeholder="输入行号"
- @pressEnter="applyHeaderRowInput"
- @blur="applyHeaderRowInput"
- />
- </div>
- </div>
- <div class="right-panel-body">
- <div ref="tableMockRef" class="table-mock">
- <div class="mock-head" :style="{ gridTemplateColumns: mockGridTemplate }">
- <span class="row-index-cell row-index-head">表头行</span>
- <span
- v-for="(header, colIndex) in rightTableHeaders"
- :key="header"
- :data-preview-col="colIndex"
- :class="{
- 'is-added': addedColumns.includes(header),
- active: selectedAddedColumn === header,
- 'is-current-col': currentColumn === colIndex,
- 'is-first-col': colIndex === 0,
- 'is-last-col': colIndex === rightTableHeaders.length - 1,
- }"
- @click="onClickHeader(header)"
- >
- <span>{{ header }}</span>
- <span
- v-if="getColumnRuleBadgeState(colIndex)"
- class="rule-badge"
- :class="`is-${getColumnRuleBadgeState(colIndex)}`"
- :title="getColumnRuleBadgeTitle(colIndex)"
- ></span>
- </span>
- </div>
- <div class="mock-match-row" :style="{ gridTemplateColumns: mockGridTemplate }">
- <span class="row-index-cell match-row-index">目标字段</span>
- <span
- v-for="(header, colIndex) in rightTableHeaders"
- :key="`mapping-${header}`"
- :data-preview-col="colIndex"
- :class="{
- 'is-current-col': currentColumn === colIndex,
- 'is-first-col': colIndex === 0,
- 'is-last-col': colIndex === rightTableHeaders.length - 1,
- 'is-empty': !getMatchedTargetFieldText(header),
- }"
- @click="onClickHeader(header)"
- >
- <span class="match-field-content">
- <span class="match-field-text">{{ getMatchedTargetFieldText(header) || '-' }}</span>
- <span class="match-status-group">
- <span
- class="match-status-badge"
- :class="getMatchedHeaderStatusClass(header)"
- :title="getMatchedHeaderStatusTitle(header) || undefined"
- >
- {{ getMatchedHeaderStatusText(header) }}
- </span>
- <span
- v-if="hasHeaderFormatError(header)"
- class="match-status-badge is-formatError"
- :title="getHeaderFormatErrorTitle(header) || undefined"
- >
- !
- </span>
- </span>
- </span>
- </span>
- </div>
- <div
- class="mock-row"
- :style="{ gridTemplateColumns: mockGridTemplate }"
- :class="{
- 'is-ignored-row': ignoredRowsSet.has(rowIndex + 1),
- 'is-current-row': selectedRowIndex === rowIndex,
- 'is-header-row': headerRowIndex === rowIndex,
- }"
- v-for="(row, rowIndex) in tablePreviewRows"
- :key="`row-${rowIndex}`"
- >
- <span class="row-index-cell" @click="selectedRowIndex = rowIndex">
- {{ rowIndex + 1 }}
- </span>
- <span
- v-for="(header, colIndex) in rightTableHeaders"
- :key="`${header}-${rowIndex}`"
- :data-preview-col="colIndex"
- :class="{
- 'is-current-col': currentColumn === colIndex,
- 'is-first-col': colIndex === 0,
- 'is-last-col': colIndex === rightTableHeaders.length - 1,
- }"
- @click="onClickCell(rowIndex, header, colIndex)"
- >
- {{ row[header] || '' }}
- </span>
- </div>
- </div>
- <div class="cleaning-rule-panel" :class="{ 'is-collapsed': !cleaningRulePanelExpanded }">
- <div class="rule-title-row">
- <div class="rule-tabs">
- <span
- v-for="(tab, idx) in rulesList"
- :key="tab.name"
- class="rule-tab"
- :class="{ active: activeRules === idx }"
- @click="rulesListCheck(idx)"
- >
- {{ tab.name }}
- </span>
- </div>
- <div class="rule-title-meta">
- <span class="current-col">当前选中列:{{ currentColumnTitle || '-' }}</span>
- <span
- class="rule-toggle"
- :class="{ expanded: cleaningRulePanelExpanded }"
- @click="cleaningRulePanelExpanded = !cleaningRulePanelExpanded"
- >
- {{ cleaningRulePanelExpanded ? '收起' : '展开' }}
- <component :is="cleaningRulePanelExpanded ? DownOutlined : UpOutlined" class="rule-toggle-icon" />
- </span>
- </div>
- </div>
- <div v-show="cleaningRulePanelExpanded" class="rule-content">
- <div class="rule-col">
- <div class="col-title">清洗规则</div>
- <div class="col-list">
- <div
- v-for="(item, idx) in visibleCleaningRules"
- :key="`${item.key}-${idx}`"
- class="col-item"
- :class="{ active: item.active }"
- @click="rulesListClick(item.key)"
- @dblclick="dbRulesListClick(item.key)"
- >
- {{ item.name }}
- </div>
- </div>
- </div>
- <div class="rule-op">
- <Button size="small" @click="rulesListRight">></Button>
- <Button size="small" @click="rulesListLeft"><</Button>
- </div>
- <div class="rule-col">
- <div class="col-title between">
- <span>已选规则</span>
- <span class="clear-btn" @click="selectRulesDel">全部删除</span>
- </div>
- <div class="col-list">
- <div
- v-for="(item, idx) in currentSelectedRules"
- :key="`${item.key}-${idx}`"
- class="col-item"
- :class="{ active: item.active }"
- @click="selectRulesClick(idx)"
- >
- <span class="col-item-label">{{ item.name }}</span>
- <span class="col-item-status" :class="{ 'is-enabled': item.enabled, 'is-disabled': !item.enabled }">
- {{ item.enabled ? '已启用' : '未启用' }}
- </span>
- </div>
- </div>
- </div>
- <div class="rule-config">
- <div class="col-title">{{
- currentActiveSelectedRule || !currentRuleExample ? '规则配置' : '示例'
- }}</div>
- <div class="config-placeholder">
- <template v-if="currentActiveSelectedRule">
- <div class="config-rule-shell">
- <div class="config-rule-head">
- <div class="config-rule-title-row">
- <div class="config-rule-copy">
- <div class="config-rule-title">
- {{ currentRuleTitle || ruleNameMap[currentActiveSelectedRule.key] }}
- </div>
- </div>
- <div class="config-rule-meta">
- <span class="config-rule-state" :class="currentRuleEnabledClass">
- {{ currentRuleEnabledLabel }}
- </span>
- <span class="config-rule-tag" :class="`is-${currentRuleScopeType}`">
- {{ currentRuleScopeLabel }}
- </span>
- <Button
- v-if="currentActiveSelectedRule.enabled"
- size="small"
- class="config-rule-toggle-btn"
- @click="toggleCurrentRuleEnabled(false)"
- >
- 停用规则
- </Button>
- </div>
- </div>
- </div>
- <template v-if="currentActiveSelectedRule.key === 'ignoreLine'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-field">
- <span class="config-field-label">忽略行号</span>
- <Input v-model:value="omitLineInput" placeholder="例如:2,5,8" />
- <div class="config-hint">
- 按 1 开始计数,多个行号使用英文逗号分隔,预览结果和左侧结构会同步忽略这些行。
- </div>
- </div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyIgnoreLineRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>输入</span>
- <code>2,5,8</code>
- </div>
- <div class="config-example-row">
- <span>效果</span>
- <span>第 2、5、8 行不会参与后续清洗与导入</span>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'dateConfig'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-field">
- <span class="config-field-label">输出类型</span>
- <Select v-model:value="dateConfigType" :options="dateTypeOptions" />
- <div class="config-hint">
- 根据源值自动识别常见日期格式,并统一转换为标准日期文本输出。
- </div>
- </div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyDateConfigRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>原始值</span>
- <code>20260324153020</code>
- </div>
- <div class="config-example-row">
- <span>日期时间</span>
- <code>2026-03-24 15:30:20</code>
- </div>
- <div class="config-example-row">
- <span>仅日期</span>
- <code>2026-03-24</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'extractData'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <Radio.Group v-model:value="extractDataMode" class="config-extract-data-group">
- <div class="config-extract-data-item">
- <Radio value="before" @change="handleExtractDataModeChange('before')">提取前</Radio>
- <Input
- v-model:value="extractDataBefore"
- :disabled="extractDataMode !== 'before'"
- placeholder="例如:6"
- />
- <span class="config-extract-data-unit">位</span>
- </div>
- <div class="config-extract-data-item">
- <Radio value="after" @change="handleExtractDataModeChange('after')">提取后</Radio>
- <Input
- v-model:value="extractDataAfter"
- :disabled="extractDataMode !== 'after'"
- placeholder="例如:4"
- />
- <span class="config-extract-data-unit">位</span>
- </div>
- </Radio.Group>
- <div class="config-hint">提取前与提取后互斥,只会应用当前选中的提取方向。</div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyExtractDataRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>原始值</span>
- <code>13800138000</code>
- </div>
- <div class="config-example-row">
- <span>前 3 位</span>
- <code>138</code>
- </div>
- <div class="config-example-row">
- <span>后 4 位</span>
- <code>8000</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'extractSegment'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-form-grid">
- <div class="config-field">
- <span class="config-field-label">分隔符</span>
- <Input v-model:value="extractSegmentValue" placeholder="例如:-" />
- </div>
- <div class="config-field">
- <span class="config-field-label">提取段位</span>
- <InputNumber v-model:value="extractSegmentRow" :min="1" :precision="0" />
- </div>
- </div>
- <div class="config-hint">按分隔符拆分后,从左到右提取指定段位内容。</div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyExtractSegmentRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>原始值</span>
- <code>四川省-成都市-高新区</code>
- </div>
- <div class="config-example-row">
- <span>分隔符</span>
- <code>-</code>
- </div>
- <div class="config-example-row">
- <span>第 2 段</span>
- <code>成都市</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'timestampConversion'">
- <div class="config-section">
- <div class="config-section-title">规则说明</div>
- <div class="config-desc-list">
- <div class="config-desc-item">自动识别 10 位或 13 位时间戳,并转换为标准时间文本。</div>
- <div class="config-desc-item">适用于毫秒时间戳、秒时间戳以及常见纯数字时间字段。</div>
- </div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyTimestampConversionRule">确认启用</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>时间戳</span>
- <code>1711209600</code>
- </div>
- <div class="config-example-row">
- <span>转换后</span>
- <code>2024-03-24 00:00:00</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'removeSpecialCharacters'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-check-list">
- <Checkbox v-model:checked="removeSpaceChecked">移除空格字符</Checkbox>
- <Checkbox v-model:checked="removeTabChecked">移除换行字符(tab)</Checkbox>
- </div>
- <div class="config-field">
- <span class="config-field-label">自定义移除字符</span>
- <div class="config-check-inline">
- <Checkbox v-model:checked="removeCustomChecked">启用</Checkbox>
- <Input
- v-model:value="removeCustomText"
- :disabled="!removeCustomChecked"
- placeholder="例如:-"
- />
- </div>
- </div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyRemoveSpecialRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>原始值</span>
- <code>138 0013-8000</code>
- </div>
- <div class="config-example-row">
- <span>清洗后</span>
- <code>13800138000</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'absoluteValue'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-check-list single">
- <Checkbox v-model:checked="absoluteValueChecked">将当前列转换为绝对值</Checkbox>
- </div>
- <div class="config-hint">仅对数值型内容生效,负数会转换为对应正值。</div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyAbsoluteValueRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>原始值</span>
- <code>-20</code>
- </div>
- <div class="config-example-row">
- <span>清洗后</span>
- <code>20</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'decimalConversion'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-check-list single">
- <Checkbox v-model:checked="decimalEnabled">启用进制转换</Checkbox>
- </div>
- <div class="config-form-grid">
- <div class="config-field">
- <span class="config-field-label">当前进制</span>
- <Select v-model:value="decimalSourceBase" :options="decimalBaseOptions" />
- </div>
- <div class="config-field">
- <span class="config-field-label">转换后进制</span>
- <Select v-model:value="decimalTargetBase" :options="decimalBaseOptions" />
- </div>
- </div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyDecimalConversionRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>原始值</span>
- <code>56(10进制)</code>
- </div>
- <div class="config-example-row">
- <span>清洗后</span>
- <code>38(16进制)</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'replaceValue'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-check-list single">
- <Checkbox v-model:checked="replaceEnabled">启用条件替换</Checkbox>
- </div>
- <div class="config-field">
- <span class="config-field-label">匹配方式</span>
- <Radio.Group v-model:value="replaceMode" class="config-radio-group">
- <Radio v-for="item in replaceModeOptions" :key="item.value" :value="item.value">
- {{ item.label }}
- </Radio>
- </Radio.Group>
- </div>
- <div class="config-form-grid">
- <div class="config-field">
- <span class="config-field-label">源值</span>
- <Input v-model:value="replaceSource" placeholder="例如:未知" />
- </div>
- <div class="config-field">
- <span class="config-field-label">目标值</span>
- <Input v-model:value="replaceTarget" placeholder="例如:--" />
- </div>
- </div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyReplaceRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>包含“未知”</span>
- <code>未知开户地址 -> --开户地址</code>
- </div>
- <div class="config-example-row">
- <span>等于“空”</span>
- <code>空 -> --</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'groupingColumns'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-hint">
- 仅建议用于新增列。按配置顺序拼接多个来源列的前/后 N 位内容,点击“应用规则”后生效。
- </div>
- <div v-if="!isCurrentColumnAdded" class="config-alert">
- 当前列不是新增列,组合列规则通常用于新增列结果字段。
- </div>
- <div class="grouping-list">
- <div class="grouping-row" v-for="(item, idx) in groupingColumnsDraft" :key="`group-${idx}`">
- <div class="config-field">
- <span class="config-field-label">来源表头</span>
- <Select
- v-model:value="item.header"
- :options="groupingHeaderOptions"
- placeholder="请选择表头"
- @change="(value) => handleGroupingColumnHeaderChange(idx, String(value || ''))"
- />
- </div>
- <div class="config-field">
- <span class="config-field-label">提取方向</span>
- <Radio.Group v-model:value="item.direction" class="config-radio-group compact">
- <Radio value="前">前</Radio>
- <Radio value="后">后</Radio>
- </Radio.Group>
- </div>
- <div class="config-field">
- <span class="config-field-label">位数</span>
- <InputNumber v-model:value="item.count" :min="1" :precision="0" />
- </div>
- <Button size="small" class="grouping-remove-btn" @click="removeGroupingColumnDraft(idx)">
- 删除
- </Button>
- </div>
- </div>
- <div class="config-actions between">
- <Button size="small" @click="addGroupingColumnDraft">新增一行</Button>
- <Button type="primary" size="small" @click="applyGroupingColumnsRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>配置方式</span>
- <code>姓名前 1 位 + 手机号后 4 位</code>
- </div>
- <div class="config-example-row">
- <span>组合结果</span>
- <code>张2369</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'magnificationAdjustment'">
- <div class="config-section">
- <div class="config-section-title">参数配置</div>
- <div class="config-field">
- <span class="config-field-label">放大倍数</span>
- <InputNumber v-model:value="magnificationValue" :min="1" />
- <div class="config-hint">
- 当前规则会按照“原值 × 倍数”输出,适合金额或比例类字段统一换算。
- </div>
- </div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyMagnificationRule">应用规则</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>原始值</span>
- <code>1.23</code>
- </div>
- <div class="config-example-row">
- <span>倍数 100</span>
- <code>123</code>
- </div>
- </div>
- </div>
- </template>
- <template v-else-if="currentActiveSelectedRule.key === 'yearComplete'">
- <div class="config-section">
- <div class="config-section-title">规则说明</div>
- <div class="config-desc-list">
- <div class="config-desc-item">当日期值缺少年份时,系统会按当前年份自动补全年份。</div>
- <div class="config-desc-item">若补全年份后的日期晚于当前时间,则会自动回退到上一年。</div>
- </div>
- <div class="config-actions">
- <Button type="primary" size="small" @click="applyYearCompleteRule">确认启用</Button>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div class="config-example-row">
- <span>原始值</span>
- <code>02-23 12:00:00</code>
- </div>
- <div class="config-example-row">
- <span>补全年份后</span>
- <code>2026-02-23 12:00:00</code>
- </div>
- </div>
- </div>
- </template>
- </div>
- </template>
- <template v-else>
- <div v-if="currentRuleExample" class="config-rule-shell is-example-preview">
- <div class="config-rule-head">
- <div class="config-rule-title-row">
- <div class="config-rule-copy">
- <div class="config-rule-title">{{ currentRuleExample.title }}</div>
- <div class="config-rule-desc">{{ currentRuleExample.description }}</div>
- </div>
- <div class="config-rule-meta">
- <span class="config-rule-state is-example">示例</span>
- <span class="config-rule-tag" :class="`is-${currentRuleScopeType}`">
- {{ currentRuleScopeLabel }}
- </span>
- </div>
- </div>
- </div>
- <div class="config-section is-example">
- <div class="config-section-title">规则示例</div>
- <div class="config-example-list">
- <div v-for="row in currentRuleExample.rows" :key="row.label" class="config-example-row">
- <span>{{ row.label }}</span>
- <code>{{ row.value }}</code>
- </div>
- </div>
- </div>
- <div class="config-preview-tip">
- 双击左侧规则或点击“>”添加到“已选规则”后,可配置参数并应用到当前列。
- </div>
- </div>
- <div v-else class="config-empty-state">
- <div class="empty-title">规则配置说明</div>
- <div class="empty-text">
- 当前规则:{{ currentRuleTitle || '未选择' }}({{ currentActiveRuleDisplayName }})
- </div>
- <div class="empty-tip">请先从左侧选择规则并添加到“已选规则”,再在此处进行参数配置。</div>
- </div>
- </template>
- </div>
- </div>
- </div>
- </div>
- </div>
- </section>
- </div>
- <Modal
- v-model:open="addColumnDialogOpen"
- :footer="null"
- :width="704"
- :centered="true"
- @cancel="resetAddColumnForm"
- wrapClassName="cleaning-config-modal add-column-modal"
- >
- <template #title>
- <div class="cleaning-modal-title">
- <span class="title-main">{{ addColumnMode === 'add' ? '新增列' : '编辑列' }}</span>
- <span class="title-desc">支持引用原始列或录入固定文本,快速生成新的目标列。</span>
- </div>
- </template>
- <div class="add-col-dialog addColumn">
- <div class="addColumn-name">
- <div class="row">
- <span class="label required">{{ addColumnMode === 'add' ? '新增列名' : '编辑列名' }}:</span>
- <Input v-model:value="addColumnForm.name" class="name-input" placeholder="请输入列名" />
- </div>
- </div>
- <div class="addColumn-tip">双击左侧原始数据列即可加入右侧新列内容,右侧支持上下调整拼接顺序。</div>
- <div class="addColumn-content">
- <div class="addColumn-content-left">
- <div class="content-card-head">
- <div class="content-left-tip">原始数据列名</div>
- <div class="content-card-desc">从当前数据表中选择可引用字段</div>
- </div>
- <div class="left-oldColumn">
- <div
- v-for="field in sourceFieldCandidates"
- :key="field"
- class="oldColumn-box"
- :title="field"
- :class="{ 'active-item': selectedSourceField === field }"
- @click="selectedSourceField = field"
- @dblclick="appendSourceField(field)"
- >
- {{ field }}
- </div>
- </div>
- </div>
- <div class="addColumn-content-right">
- <div class="right-header">
- <div class="content-card-head">
- <div class="content-right-tip">新列内容</div>
- <div class="content-card-desc">
- {{ addColumnForm.mode === 'ref' ? '支持拖动排序并删除单个字段' : '可直接输入固定文本作为新列内容' }}
- </div>
- </div>
- </div>
- <div class="right-radio">
- <Radio.Group v-model:value="addColumnForm.mode">
- <Radio value="ref">引用已有列</Radio>
- <Radio value="custom">自定义列内容</Radio>
- </Radio.Group>
- </div>
- <div class="right-newColumn" v-show="addColumnForm.mode === 'ref'">
- <div
- v-for="(field, index) in addColumnForm.contentFields"
- :key="`${field}-${index}`"
- class="newColumn-box"
- :class="{
- 'active-item': selectedContentIndex === index,
- 'is-dragging': draggingContentIndex === index,
- 'is-drag-over': dragOverContentIndex === index && draggingContentIndex !== index,
- }"
- draggable="true"
- @click="selectedContentIndex = index"
- @dragstart="handleContentDragStart(index, $event)"
- @dragenter="handleContentDragEnter(index)"
- @dragover="handleContentDragOver(index, $event)"
- @drop="handleContentDrop(index, $event)"
- @dragend="handleContentDragEnd"
- >
- <span class="newColumn-text" :title="field">{{ field }}</span>
- <span class="del-icon" @click.stop="removeContentField(index)">×</span>
- </div>
- </div>
- <div class="right-input" v-show="addColumnForm.mode === 'custom'">
- <Input
- v-model:value="addColumnForm.customContent"
- :maxlength="100"
- placeholder="请输入自定义列内容"
- style="height: 32px; line-height: 32px"
- />
- </div>
- </div>
- </div>
- <div class="dialog-footer">
- <Button class="add-col-cancel" @click="resetAddColumnForm">取消</Button>
- <Button type="primary" class="add-col-confirm" @click="confirmAddOrEditColumn">确定</Button>
- </div>
- </div>
- </Modal>
- <Modal
- v-model:open="splitColumnDialogOpen"
- :width="460"
- :centered="true"
- okText="确定"
- cancelText="取消"
- @ok="confirmSplitColumn"
- @cancel="resetSplitForm"
- wrapClassName="cleaning-config-modal split-column-modal"
- >
- <template #title>
- <div class="cleaning-modal-title">
- <span class="title-main">拆分列</span>
- <span class="title-desc">按字符长度或特殊标识将单列拆分为两个目标列,适合姓名、地址等复合字段。</span>
- </div>
- </template>
- <div class="split-column-form">
- <div class="split-column-tip">选择需要拆分的原始列,并填写拆分后的列名与拆分截止标识。</div>
- <div class="split-form-card">
- <div class="form-row">
- <span class="form-label">选择需要拆分的列:</span>
- <Select
- v-model:value="splitColumnForm.source"
- :options="splitSourceOptions"
- placeholder="请选择"
- class="form-input"
- />
- </div>
- <div class="form-row">
- <span class="form-label required">拆分后列名1:</span>
- <Input v-model:value="splitColumnForm.targetName1" placeholder="请输入" class="form-input" />
- </div>
- <div class="form-row">
- <span class="form-label required">拆分后列名2:</span>
- <Input v-model:value="splitColumnForm.targetName2" placeholder="请输入" class="form-input" />
- </div>
- <div class="form-row">
- <span class="form-label required">拆分截止标识:</span>
- <div class="delimiter-group">
- <Select
- v-model:value="splitColumnForm.delimiterType"
- :options="delimiterOptions"
- class="delimiter-select"
- />
- <Input
- v-model:value="splitColumnForm.delimiterValue"
- :placeholder="splitColumnForm.delimiterType === 'length' ? '请输入字符长度' : '请输入特殊标识'"
- class="delimiter-input"
- />
- </div>
- </div>
- </div>
- </div>
- </Modal>
- <Modal v-model:open="deleteColumnDialogOpen" title="删除列" :width="520" :centered="true" @ok="confirmDeleteColumn">
- <div>确认删除列「{{ selectedAddedColumn || '-' }}」?</div>
- </Modal>
- <Modal
- v-model:open="templateModalOpen"
- :footer="null"
- width="1080px"
- :centered="true"
- wrapClassName="select-template-modal"
- :destroyOnClose="true"
- >
- <template #title>
- <div class="cleaning-modal-title">
- <span class="title-main">选择模板</span>
- </div>
- </template>
- <div class="template-modal-layout">
- <aside class="modal-left">
- <div class="modal-panel-head">
- <div>
- <div class="panel-title">模板分类</div>
- </div>
- <span class="head-badge">{{ Math.max(categories.length - 1, 0) }} 类</span>
- </div>
- <Button type="primary" block class="create-template-btn">+创建新模板</Button>
- <div class="cate-list">
- <div
- v-for="cate in categories"
- :key="cate"
- class="cate-item"
- :class="{ active: activeCategory === cate }"
- @click="activeCategory = cate"
- >
- <span class="cate-name">{{ cate }}</span>
- <span class="cate-count">{{ getCategoryCount(cate) }}</span>
- </div>
- </div>
- </aside>
- <section class="modal-mid">
- <div class="modal-panel-head">
- <div>
- <div class="panel-title">模板列表</div>
- </div>
- <div class="modal-panel-head__actions">
- <div class="tab-row" v-show="showTemplateTabSwitch">
- <span
- class="tab"
- :class="{ active: activeTemplateTab === 'template' }"
- @click="activeTemplateTab = 'template'"
- >模板</span
- >
- <span
- class="tab"
- :class="{ active: activeTemplateTab === 'plugin' }"
- @click="activeTemplateTab = 'plugin'"
- >模板插件</span
- >
- </div>
- <span class="head-badge">{{ filteredTemplateList.length }} 个</span>
- </div>
- </div>
- <div class="filters">
- <div class="filter-item filter-item--grow">
- <span class="filter-label">模板名称</span>
- <Input v-model:value="templateKeyword" placeholder="输入模板名称查询" />
- </div>
- <div class="filter-item">
- <span class="filter-label">来源</span>
- <Select v-model:value="templateSource" :options="templateSourceOptions" class="source-select" />
- </div>
- </div>
- <div class="list-summary">
- <span>当前范围:{{ activeTemplateTab === 'plugin' ? '模板插件' : activeCategory }}</span>
- <span>已选:{{ modalTemplateItem ? getTemplateDisplayName(modalTemplateItem) : '未选择' }}</span>
- </div>
- <div class="tpl-list">
- <div
- v-for="tpl in filteredTemplateList"
- :key="String(tpl.id)"
- class="tpl-item"
- :class="{ active: modalTemplateItem?.id === tpl.id }"
- @click="handleTemplateItemClick(tpl)"
- @dblclick="handleTemplateItemConfirm(tpl)"
- >
- <div class="tpl-main">
- <div class="name">{{ getTemplateDisplayName(tpl) }}</div>
- <div class="tpl-meta">
- <span class="tpl-meta-text">{{ tpl.classify || '未分类' }}</span>
- <span class="tpl-meta-text">{{ getTemplateSourceLabel(tpl) }}</span>
- <span class="tpl-meta-text">表头 {{ getTemplateHeaderCount(tpl) }} 项</span>
- </div>
- </div>
- <span v-if="modalTemplateItem?.id === tpl.id" class="selected-mark">已选</span>
- </div>
- <div v-if="!filteredTemplateList.length" class="empty">暂无模板</div>
- </div>
- </section>
- <section class="modal-right">
- <div class="modal-panel-head">
- <div>
- <div class="panel-title">模板规则预览</div>
- </div>
- <span class="head-badge">{{ previewRules.length }} 条</span>
- </div>
- <template v-if="modalPreviewTemplate">
- <div class="preview-overview">
- <div class="overview-card">
- <span class="overview-label">模板名称</span>
- <span class="overview-value">{{ getTemplateDisplayName(modalPreviewTemplate) }}</span>
- </div>
- <div class="overview-card">
- <span class="overview-label">模板来源</span>
- <span class="overview-value">{{ modalTemplateSourceLabel }}</span>
- </div>
- <div class="overview-card">
- <span class="overview-label">分类</span>
- <span class="overview-value">{{ modalPreviewTemplate.classify || '未分类' }}</span>
- </div>
- <div class="overview-card">
- <span class="overview-label">字段数</span>
- <span class="overview-value">{{ modalTemplateFieldCount }}</span>
- </div>
- </div>
- <div class="preview-meta">
- <div class="preview-meta-row preview-meta-row--headers">
- <span class="meta-label">表头预览</span>
- <span class="meta-value meta-value--scroll">
- {{ modalTemplateHeaderList.length ? modalTemplateHeaderList.join(' / ') : '暂无表头信息' }}
- </span>
- </div>
- </div>
- <div class="preview-rule-title">模板规则</div>
- <div class="preview-rule-table">
- <div class="rule-head">
- <span>目标字段</span>
- <span>原始字段</span>
- </div>
- <div class="rule-row" v-for="(row, idx) in previewRules" :key="idx">
- <span class="rule-cell rule-cell--target">
- <span class="rule-target-label" :title="row.targetField">
- <span v-if="row.required" class="rule-required-mark">*</span>
- <span class="rule-target-text">{{ row.targetField }}</span>
- </span>
- <span v-if="row.required" class="rule-required-badge">必填</span>
- </span>
- <span class="rule-cell" :title="row.originalField">{{ row.originalField }}</span>
- </div>
- <div v-if="templatePreviewLoading" class="empty">模板详情加载中...</div>
- <div v-else-if="!previewRules.length" class="empty">暂无规则</div>
- </div>
- </template>
- <template v-else>
- <div class="preview-empty">
- <div class="empty-title">请选择模板</div>
- <div class="empty-desc">左侧选择分类,中间点击具体模板后,这里会展示字段规则与基础信息。</div>
- </div>
- </template>
- <div class="modal-actions">
- <Button type="primary" :disabled="!modalTemplateItem || isPluginTemplateTab" @click="confirmTemplateSelect">
- 确认选择
- </Button>
- <Button @click="closeTemplateModal">取消</Button>
- </div>
- </section>
- </div>
- </Modal>
- </div>
- </template>
- <script lang="ts" setup name="ViewsDataCleaning">
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
- import { DownOutlined, UpOutlined } from '@ant-design/icons-vue';
- import { Button, Checkbox, Input, InputNumber, Modal, Popover, Radio, Select, Switch, Tooltip } from 'ant-design-vue';
- import { EmptyState } from '@/core/components/EmptyState';
- import { BasicTree } from '@/core/components/Tree';
- import { useDesign } from '@/core/hooks/web/useDesign';
- import { useGo, useQuery } from '@/core/hooks/web/usePage';
- import { PageEnum } from '@/core/enums/pageEnum';
- import { useMessage } from '@/core/hooks/web/useMessage';
- import type { FileInfo } from '@/types/api';
- import { tplCategoryList, tplCheckTableName, tplDetail, tplPage } from '@/plat/api/tpl/tplApi';
- import { rawPreview } from '@/case/api/govern/governApi';
- import type { TableInfo } from '@/plat/api/tpl/types/dto';
- import {
- FunAbs,
- FunDate,
- FunDateTime,
- FunExtra,
- FunMultiplier,
- FunRadix,
- FunRegular,
- FunReplace,
- FunSpExtra,
- FunTime,
- MappingEnum,
- RuleMapping,
- RuleSplit,
- } from '../../../dm/types/Func';
- import {
- buildRemoveSpecialRegex,
- groupColumnsFnValue,
- normalizeDecimalBaseLabel,
- runCleaningPreviewPipeline,
- type CleaningRuleKey as RulePipelineKey,
- } from './cleaningRuleUtils';
- import { resetImportProgressSession } from './importProgressSession';
- import { FileStatusEnum } from '../../types/enum';
- type TreeTableRuleInfo = {
- id?: string | number | null;
- tableId?: string | number | null;
- fieldNameCn?: string | null;
- fieldNameEn?: string | null;
- fieldType?: string | null;
- required?: number | null;
- fieldLen?: number | null;
- matched?: number | null;
- matchedRequired?: number | null;
- directionConf?: string | null;
- zf?: number | string | boolean | null;
- fieldSort?: number | null;
- fileColIndex?: number | null;
- fileColName?: string | null;
- funList?: TreeStoredFunRule[] | null;
- columnRule?: TreeStoredColumnRule | null;
- };
- type TreeStoredFunRule = {
- '@class'?: string;
- key?: string;
- ruleKey?: string;
- name?: string;
- sort?: number | null;
- rules?: Record<string, any>;
- [key: string]: any;
- };
- type TreeStoredColumnRule = {
- '@class'?: string;
- mode?: 'ref' | 'custom' | 'split';
- contentFields?: string[];
- customContent?: string;
- source?: string;
- delimiterType?: string;
- delimiterValue?: string;
- targetName1?: string;
- targetName2?: string;
- refIndex?: number | number[] | null;
- mappingEnum?: number | null;
- splitNameIndex?: number | null;
- splitNameIndex2?: number | null;
- content?: string | null;
- len?: number | null;
- splitChar?: string | null;
- [key: string]: any;
- };
- type TreeFileInfo = Omit<
- FileInfo,
- | 'id'
- | 'pid'
- | 'children'
- | 'tableName'
- | 'templateId'
- | 'fileType'
- | 'dataNum'
- | 'fileSize'
- | 'fileStatus'
- | 'lineNo'
- | 'batchId'
- | 'successNum'
- | 'failNum'
- | 'charSet'
- | 'delimiter'
- | 'header'
- | 'tableRuleList'
- > & {
- id?: string | number | null;
- pid?: string | number | null;
- tableName?: string | null;
- templateId?: string | number | null;
- fileType?: string | null;
- dataNum?: string | number | null;
- fileSize?: string | number | null;
- fileStatus?: string | null;
- lineNo?: number | null;
- batchId?: string | number | null;
- successNum?: string | number | null;
- failNum?: string | number | null;
- charSet?: string | null;
- delimiter?: string | null;
- header?: string | string[] | null;
- tableRuleList?: TreeTableRuleInfo[] | null;
- ignoreLines?: number[] | null;
- groupColumns?: Array<{ targetHeader: string; groupFlag: boolean; groupColumns: Array<Record<string, any>> }> | null;
- funcRegx?: string | null;
- childTemplateName?: string | null;
- children?: TreeFileInfo[];
- };
- type PreviewFileDataset = {
- headers: string[];
- rows: Record<string, string>[];
- };
- type TemplateListItem = TableInfo & { source?: number; hasInner?: string | number | boolean | null };
- type MappingTargetField = { key: string; label: string; required: boolean };
- type FormatValidationType =
- | 'number'
- | 'date'
- | 'dateTime'
- | 'time'
- | 'loanFlag'
- | 'callType'
- | 'phone'
- | 'acctCardNo'
- | 'idCardNumber';
- type RecognitionDirectionComparableMap = Record<RecognitionRuleTabKey, Set<string>>;
- type HeaderFormatValidationState = {
- invalidTargets: Array<{
- key: string;
- label: string;
- formatType: FormatValidationType;
- invalidValue: string;
- }>;
- title: string;
- };
- type RecognitionBasicInfoRow = {
- key: string;
- headerMatch: string;
- targetField: string;
- endFlag: string;
- };
- type RecognitionDirectionItem = {
- key: string;
- label: string;
- value: string;
- };
- type RecognitionRuleTabKey = '借贷标志' | '通信类别';
- type RecognitionDirectionField = {
- key: string;
- label: string;
- tab: RecognitionRuleTabKey;
- entries: RecognitionDirectionItem[];
- };
- const { prefixCls } = useDesign('case-data-cleaning');
- const go = useGo();
- const pageQuery = useQuery();
- const { createMessage } = useMessage();
- const importingData = ref(false);
- const leftCollapsed = ref(false);
- const leftPanelWidth = ref(300);
- const middlePanelWidth = ref(520);
- const minLeftWidth = 220;
- const maxLeftWidth = 520;
- const minMiddleWidth = 360;
- const maxMiddleWidth = 900;
- const minRightWidth = 560;
- const resizeState = ref<{ type: 'left' | 'middle' | ''; startX: number; startLeft: number; startMiddle: number }>({
- type: '',
- startX: 0,
- startLeft: 300,
- startMiddle: 520,
- });
- const pendingClientX = ref<number | null>(null);
- const rafId = ref<number | null>(null);
- const layoutTemplateColumns = computed(() => {
- if (leftCollapsed.value) {
- return `${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
- }
- return `${leftPanelWidth.value}px 6px ${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
- });
- const sortMode = ref<'asc' | 'desc' | ''>('');
- const selectedKeys = ref<string[]>([]);
- interface FileTreeNode {
- key: string;
- title: string;
- children?: FileTreeNode[];
- raw?: TreeFileInfo;
- selectable?: boolean;
- }
- type SheetNodeVisualState = 'success' | 'fail' | 'analyzing';
- const fileStatusLabelMap = Object.entries(FileStatusEnum).reduce(
- (acc, [key, label]) => {
- acc[key] = label;
- return acc;
- },
- {} as Record<string, string>,
- );
- const normalizeNodeId = (value?: string | number | null, fallback = '') => {
- const normalized = String(value ?? '').trim();
- return normalized || fallback;
- };
- const normalizeParentId = (value?: string | number | null) => {
- const normalized = String(value ?? '').trim();
- return !normalized || normalized === '0' ? '' : normalized;
- };
- const isFileRootItem = (item: TreeFileInfo) => !normalizeParentId(item.pid);
- const hasChildItems = (item: TreeFileInfo) => Array.isArray(item.children) && item.children.length > 0;
- const getFallbackTreeKey = (item: TreeFileInfo) =>
- [
- item.filePath || item.fileName || 'file',
- item.sheetName || 'root',
- item.tableName || '',
- item.lineNo ?? '',
- item.createDateTime || '',
- ].join('__');
- const getTreeNodeKey = (item: TreeFileInfo) => normalizeNodeId(item.id, getFallbackTreeKey(item));
- const normalizeTemplateId = (value?: string | number | null) => {
- const next = Number(value);
- return Number.isFinite(next) && next > 0 ? next : 0;
- };
- const getRequestErrorMessage = (error: any, fallback = '操作失败') =>
- error?.response?.data?.message || error?.message || fallback;
- const normalizeTemplateHeaders = (headers?: string | string[] | null) => {
- if (Array.isArray(headers)) {
- return headers.map((item) => String(item ?? '').trim()).filter(Boolean);
- }
- return String(headers || '')
- .split(/[,,]/)
- .map((item) => item.trim())
- .filter(Boolean);
- };
- const createTemplateFieldKey = (value: string | undefined, index: number) => {
- const normalized = String(value || '').trim();
- return normalized ? `field_${index + 1}_${normalized}` : `field_${index + 1}`;
- };
- const normalizeRuleText = (value?: unknown) => String(value ?? '').trim();
- const normalizeTemplateClassifyText = (value?: unknown) => normalizeRuleText(value);
- const normalizeTemplateNameKey = (value?: unknown) => normalizeRuleText(value).toLocaleLowerCase();
- const normalizeRecognitionConfigText = (value?: unknown) => String(value ?? '').trim();
- const normalizeSingleLineText = (value?: unknown) =>
- String(value ?? '')
- .replace(/[\t\r\n\s\u3000]+/g, '')
- .trim();
- const resolveTemplateNameCheckResult = (payload: unknown): boolean | null => {
- if (typeof payload === 'boolean') return payload;
- if (typeof payload === 'number') {
- if (payload === 1) return true;
- if (payload === 0) return false;
- return null;
- }
- if (typeof payload === 'string') {
- const normalized = payload.trim().toLowerCase();
- if (['true', '1', 'yes', 'y', 'ok', 'success'].includes(normalized)) return true;
- if (['false', '0', 'no', 'n', 'fail'].includes(normalized)) return false;
- return null;
- }
- if (!payload || typeof payload !== 'object') return null;
- const record = payload as Record<string, unknown>;
- for (const key of ['valid', 'available', 'pass', 'result', 'data']) {
- const resolved = resolveTemplateNameCheckResult(record[key]);
- if (resolved !== null) return resolved;
- }
- const existsResult = resolveTemplateNameCheckResult(record.exists);
- if (existsResult !== null) return !existsResult;
- return null;
- };
- const createRecognitionBasicInfoKey = () => `merge_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
- const normalizeFieldMatchText = (value?: string | number | null) =>
- normalizeRuleText(value)
- .replace(/[\s_\-—()()[\]【】{}<>《》"'`‘’“”、,,.。::;;/\\|]/g, '')
- .toLowerCase();
- const normalizeTableRuleList = (list?: TreeTableRuleInfo[] | null) =>
- Array.isArray(list)
- ? list.filter(
- (item) =>
- item && [item.fieldNameCn, item.fieldNameEn, item.fileColName].some((val) => normalizeRuleText(val)),
- )
- : [];
- const getIntegerRuleFlag = (value: unknown) => {
- const normalizedValue = Number(value);
- return Number.isInteger(normalizedValue) ? normalizedValue : null;
- };
- const getRawRuleFlagValue = (
- source: unknown,
- key: 'required' | 'matched' | 'matchedRequired' | 'zf',
- ): number | string | null | undefined => {
- const record = (source || {}) as Record<string, any>;
- if (!Object.prototype.hasOwnProperty.call(record, key)) return undefined;
- const value = record[key];
- if (value === undefined) return undefined;
- if (value === null) return null;
- const normalizedValue = Number(value);
- return Number.isInteger(normalizedValue) ? normalizedValue : value;
- };
- const pickFirstDefinedRuleFlagValue = (...values: Array<number | string | null | undefined>) => {
- for (const value of values) {
- if (value !== undefined) {
- return value;
- }
- }
- return undefined;
- };
- /**
- * 解析规则里的「正负转换」开关(zf)。
- *
- * <p>后端 TableRuleDTO.zf 是包装类型 Integer,清洗器以 `rule.getZf() == 1` 判断(会自动拆箱),
- * 一旦这里输出 undefined,JSON 序列化会丢掉整个键 → 后端拿到 null → 清洗任务直接抛 NPE。
- * 因此本函数保证**始终返回整数**:优先模板字段的 zf,其次上一条规则的 zf,兜底 0。
- */
- const resolveStoredZfFlag = (templateField?: unknown, previous?: TreeTableRuleInfo | null) =>
- getIntegerRuleFlag(
- pickFirstDefinedRuleFlagValue(getRawRuleFlagValue(templateField, 'zf'), getRawRuleFlagValue(previous, 'zf'), 0),
- ) ?? 0;
- const getTableRuleRequiredFlag = (item?: TreeTableRuleInfo | null, fallback = false) => {
- if (!item) return fallback;
- const required = getIntegerRuleFlag(item.required);
- if (required !== null) {
- return required === 1;
- }
- return fallback;
- };
- const getTableRuleMatchedDisplayFlag = (item?: TreeTableRuleInfo | null, fallback = false) => {
- if (!item) return fallback;
- const matched = getIntegerRuleFlag(item.matched);
- if (matched !== null) {
- return matched === 1;
- }
- const required = getIntegerRuleFlag(item.required);
- if (required !== null) {
- return required === 1;
- }
- return fallback;
- };
- const getTableRuleTargetLabel = (item: TreeTableRuleInfo, index: number) =>
- normalizeRuleText(item.fieldNameCn) || normalizeRuleText(item.fieldNameEn) || `字段${index + 1}`;
- const getTableRuleTargetKey = (item: TreeTableRuleInfo, index: number) =>
- normalizeRuleText(item.fieldNameEn) || createTemplateFieldKey(getTableRuleTargetLabel(item, index), index);
- const buildTargetFieldsFromRuleList = (list: TreeTableRuleInfo[]): MappingTargetField[] => {
- const usedKeys = new Set<string>();
- return normalizeTableRuleList(list).reduce((acc, item, index) => {
- const key = getTableRuleTargetKey(item, index);
- if (!key || usedKeys.has(key)) return acc;
- usedKeys.add(key);
- acc.push({
- key,
- label: getTableRuleTargetLabel(item, index),
- required: getTableRuleRequiredFlag(item),
- });
- return acc;
- }, [] as MappingTargetField[]);
- };
- const getTemplateFieldRequired = (value: unknown) => {
- if (value === null || value === undefined || value === '') return false;
- if (typeof value === 'boolean') return value;
- const parsed = Number(value);
- return !Number.isNaN(parsed) && parsed === 1;
- };
- const getTemplateFieldRecord = (item: unknown) => (item || {}) as Record<string, any>;
- const getTemplateFieldRequiredFlag = (item: unknown, fallback = false) => {
- const field = getTemplateFieldRecord(item);
- if (Object.prototype.hasOwnProperty.call(field, 'required')) {
- return getTemplateFieldRequired(field.required);
- }
- return fallback;
- };
- const getTemplateFieldMatchedDisplayFlag = (item: unknown, fallback = false) => {
- const field = getTemplateFieldRecord(item);
- if (Object.prototype.hasOwnProperty.call(field, 'matched')) {
- return getTemplateFieldRequired(field.matched);
- }
- if (Object.prototype.hasOwnProperty.call(field, 'required')) {
- return getTemplateFieldRequired(field.required);
- }
- return fallback;
- };
- const getOrderedTemplateFields = (fields?: TableInfo['tableFields'] | null) =>
- (Array.isArray(fields) ? fields : [])
- .filter((item) => {
- const field = getTemplateFieldRecord(item);
- return !!(field.fieldComment || field.fieldNameCn || field.fieldNameEn || field.fieldName);
- })
- .map((item, index) => {
- const field = getTemplateFieldRecord(item);
- const sort = Number(field.fieldSort);
- return {
- item,
- index,
- required: getTemplateFieldRequiredFlag(item),
- sort: Number.isFinite(sort) && sort > 0 ? sort : index + 1,
- };
- })
- .sort((a, b) => {
- if (a.required !== b.required) {
- return a.required ? -1 : 1;
- }
- return a.sort === b.sort ? a.index - b.index : a.sort - b.sort;
- })
- .map(({ item }) => item);
- const getTemplateFieldTargetLabel = (item: unknown, index: number) => {
- const field = getTemplateFieldRecord(item);
- return String(field.fieldComment || field.fieldNameCn || field.fieldName || `字段${index + 1}`);
- };
- const getTemplateFieldTargetKey = (item: unknown, index: number) => {
- const field = getTemplateFieldRecord(item);
- return String(
- field.fieldNameEn ||
- field.fieldName ||
- createTemplateFieldKey(String(field.fieldComment || field.fieldNameCn || ''), index),
- );
- };
- const isTransAmountTargetField = (field: Pick<MappingTargetField, 'key' | 'label'>) =>
- normalizeRuleText(field.key) === 'transAmount' || normalizeFieldMatchText(field.label).includes('交易金额');
- const isLoanFlagTargetField = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
- const targetKey = normalizeRuleText(field.key);
- const targetLabel = normalizeRuleText(field.label);
- return targetKey === 'debitCreditFlag' || targetKey === 'loanFlag' || targetLabel.includes('借贷');
- };
- const getZfFlag = (value: unknown) => {
- if (typeof value === 'boolean') return value;
- const normalizedValue = normalizeRuleText(value as string | number | null).toLowerCase();
- return ['1', 'true', 'yes', '是'].includes(normalizedValue);
- };
- const getRecordZfValue = (source: unknown) => {
- const record = (source || {}) as Record<string, any>;
- if (!Object.prototype.hasOwnProperty.call(record, 'zf')) return undefined;
- return record.zf === null || record.zf === undefined || record.zf === '' ? undefined : record.zf;
- };
- const getTableRuleTargetField = (item: TreeTableRuleInfo, index: number) => ({
- key: getTableRuleTargetKey(item, index),
- label: getTableRuleTargetLabel(item, index),
- });
- const findTransAmountTableRule = (list?: TreeTableRuleInfo[] | null) =>
- normalizeTableRuleList(list).find((item, index) =>
- isTransAmountTargetField(getTableRuleTargetField(item, index)),
- ) || null;
- const findTransAmountTemplateFieldMeta = (template?: Partial<TableInfo> | null) =>
- getOrderedTemplateFields(template?.tableFields).find((item, index) =>
- isTransAmountTargetField({
- key: getTemplateFieldTargetKey(item, index),
- label: getTemplateFieldTargetLabel(item, index),
- }),
- ) || null;
- const buildTargetFieldsFromTemplateInfo = (template?: Partial<TableInfo> | null): MappingTargetField[] => {
- const tableFields = getOrderedTemplateFields(template?.tableFields);
- if (tableFields.length) {
- return tableFields.map((item, index) => ({
- key: getTemplateFieldTargetKey(item, index),
- label: getTemplateFieldTargetLabel(item, index),
- required: getTemplateFieldRequiredFlag(item),
- }));
- }
- return [];
- };
- const inferTemplateClassify = (item: Pick<TreeFileInfo, 'fileName' | 'tableName' | 'sheetName'>) => {
- const text = `${item.fileName || ''}${item.tableName || ''}${item.sheetName || ''}`;
- if (text.includes('通信')) return '通信类';
- if (text.includes('银行') || text.includes('账户') || text.includes('交易')) return '金融类';
- if (text.includes('民航') || text.includes('铁路') || text.includes('旅馆') || text.includes('快递'))
- return '轨迹类';
- if (text.includes('公安') || text.includes('户籍') || text.includes('违法') || text.includes('护照'))
- return '公安类';
- return '未分类';
- };
- const FILE_INFO_STORAGE_KEY = 'case_clean_file_infos';
- const IMPORT_FILE_INFO_STORAGE_KEY = 'case_clean_import_file_infos';
- const mockFileList: TreeFileInfo[] = [];
- const buildTreeFileInfoIdMap = (list: TreeFileInfo[]) => {
- const infoMap = new Map<string, TreeFileInfo>();
- const walk = (nodes?: TreeFileInfo[] | null) => {
- if (!Array.isArray(nodes)) return;
- nodes.forEach((node) => {
- const nodeId = normalizeRuleText(node?.id);
- if (nodeId) {
- infoMap.set(nodeId, node);
- }
- if (Array.isArray(node?.children) && node.children.length) {
- walk(node.children);
- }
- });
- };
- walk(list);
- return infoMap;
- };
- const mockFileInfoById = buildTreeFileInfoIdMap(mockFileList);
- const mergeStoredTableRuleListWithFallback = (
- storedList?: TreeTableRuleInfo[] | null,
- fallbackList?: TreeTableRuleInfo[] | null,
- ) => {
- const storedRules = normalizeTableRuleList(storedList);
- const fallbackRules = normalizeTableRuleList(fallbackList);
- if (!storedRules.length) {
- return fallbackRules.map((item) => sanitizeTreeTableRuleInfo(item));
- }
- if (!fallbackRules.length) {
- return storedRules.map((item) => sanitizeTreeTableRuleInfo(item));
- }
- const fallbackRuleMap = fallbackRules.reduce(
- (acc, item, index) => {
- const key = getTableRuleTargetKey(item, index);
- const label = normalizeFieldMatchText(getTableRuleTargetLabel(item, index));
- if (key) acc[key] = item;
- if (label) acc[label] = item;
- return acc;
- },
- {} as Record<string, TreeTableRuleInfo>,
- );
- return storedRules.map((item, index) => {
- const key = getTableRuleTargetKey(item, index);
- const label = normalizeFieldMatchText(getTableRuleTargetLabel(item, index));
- const fallbackRule = (key && fallbackRuleMap[key]) || (label && fallbackRuleMap[label]) || null;
- return sanitizeTreeTableRuleInfo({
- ...cloneDeepSafe(fallbackRule || {}),
- ...cloneDeepSafe(item),
- } as TreeTableRuleInfo);
- });
- };
- const mergeStoredFileInfoWithMock = (item: TreeFileInfo): TreeFileInfo => {
- const mockItem = mockFileInfoById.get(normalizeRuleText(item?.id));
- const mergedChildrenSource = Array.isArray(item.children) ? item.children : mockItem?.children;
- return sanitizeTreeFileInfo({
- ...cloneDeepSafe(mockItem || item),
- ...cloneDeepSafe(item),
- filePath: normalizeRuleText(mockItem?.filePath) || item.filePath,
- tableRuleList: mergeStoredTableRuleListWithFallback(item.tableRuleList, mockItem?.tableRuleList),
- children: Array.isArray(mergedChildrenSource)
- ? mergedChildrenSource.map((child) => mergeStoredFileInfoWithMock(child))
- : [],
- });
- };
- const buildMockTemplateList = (list: TreeFileInfo[]): TemplateListItem[] => {
- const templateMap = new Map<number, TemplateListItem>();
- list.forEach((item) => {
- const templateId = normalizeTemplateId(item.templateId);
- if (!templateId || templateMap.has(templateId)) return;
- const tableRules = normalizeTableRuleList(item.tableRuleList);
- const headers = normalizeTemplateHeaders(item.header);
- templateMap.set(templateId, {
- id: templateId,
- tableNameCn: item.tableName || item.sheetName || `模板${templateId}`,
- tableNameEn: `template_${templateId}`,
- classify: inferTemplateClassify(item),
- headers: headers.join(','),
- funcRegx: normalizeRecognitionConfigText(item.funcRegx) || undefined,
- tableFields: tableRules.length
- ? tableRules.map((rule, index) => ({
- fieldName: getTableRuleTargetKey(rule, index),
- fieldType: rule.fieldType || 'string',
- fieldComment: getTableRuleTargetLabel(rule, index),
- required: getTableRuleRequiredFlag(rule) ? 1 : 0,
- matched: getTableRuleMatchedDisplayFlag(rule) ? 1 : 0,
- }))
- : headers.map((header, index) => ({
- fieldName: createTemplateFieldKey(header, index),
- fieldType: 'string',
- fieldComment: header,
- })),
- source: 1,
- });
- });
- return Array.from(templateMap.values());
- };
- const fileList = ref<TreeFileInfo[]>([...mockFileList]);
- const mockTemplateList = buildMockTemplateList(mockFileList);
- const mergeTemplateCollections = (...collections: TemplateListItem[][]) => {
- const templateMap = new Map<string, TemplateListItem>();
- collections.forEach((collection) => {
- collection.forEach((item, index) => {
- const templateId = normalizeTemplateId(item?.id);
- const key = templateId ? String(templateId) : `${item?.tableNameCn || 'template'}_${index}`;
- templateMap.set(key, item);
- });
- });
- return Array.from(templateMap.values());
- };
- const getFileStatusLabel = (status?: string | null) => {
- const normalized = String(status || '').trim();
- if (!normalized) return '待处理';
- return fileStatusLabelMap[normalized] || normalized;
- };
- const getFileStatusVisualState = (status?: string | null) => {
- const normalized = String(status || '').trim();
- if (
- normalized === 'SHEET_TEMPLATE_MATCHED_SUCCESS' ||
- normalized === FileStatusEnum.SHEET_TEMPLATE_MATCHED_SUCCESS
- ) {
- return 'success' as const;
- }
- if (normalized === 'SHEET_TEMPLATE_MATCHED_FAIL' || normalized === FileStatusEnum.SHEET_TEMPLATE_MATCHED_FAIL) {
- return 'fail' as const;
- }
- return null;
- };
- const isTemplateMatchedFailStatus = (status?: string | null) =>
- ['SHEET_TEMPLATE_MATCHED_FAIL', FileStatusEnum.SHEET_TEMPLATE_MATCHED_FAIL].includes(String(status || '').trim());
- const isTreeFileNode = (item?: TreeFileInfo | null): item is TreeFileInfo => !!item && isFileRootItem(item);
- const isTreeSheetNode = (item?: TreeFileInfo | null): item is TreeFileInfo => !!item && !isFileRootItem(item);
- const isTableRuleMatched = (
- item?: TreeTableRuleInfo | null,
- options: { index?: number; amountZfEnabled?: boolean } = {},
- ) => {
- if (!item) return false;
- if (
- options.amountZfEnabled &&
- options.index !== undefined &&
- isLoanFlagTargetField(getTableRuleTargetField(item, options.index))
- ) {
- return true;
- }
- const fileColIndex = Number(item.fileColIndex);
- return (Number.isInteger(fileColIndex) && fileColIndex >= 0) || !!normalizeRuleText(item.fileColName);
- };
- const isMatchSuccessful = (requiredTotal: number, requiredMatched: number, matchedCount: number) => {
- if (requiredTotal > 0) {
- return requiredMatched === requiredTotal;
- }
- return matchedCount > 0;
- };
- const getTableRuleMatchStats = (list?: TreeTableRuleInfo[] | null) => {
- const normalizedRules = normalizeTableRuleList(list);
- const amountZfEnabled = normalizedRules.some(
- (item, index) => isTransAmountTargetField(getTableRuleTargetField(item, index)) && getZfFlag(item.zf),
- );
- const normalizedRuleItems = normalizedRules.map((item, index) => ({ item, index }));
- const requiredRules = normalizedRuleItems.filter(({ item }) => getTableRuleRequiredFlag(item));
- const requiredTotal = requiredRules.length;
- const requiredMatched = requiredRules.filter(({ item, index }) =>
- isTableRuleMatched(item, { index, amountZfEnabled }),
- ).length;
- const matchedCount = normalizedRuleItems.filter(({ item, index }) =>
- isTableRuleMatched(item, { index, amountZfEnabled }),
- ).length;
- return { requiredTotal, requiredMatched, matchedCount };
- };
- const isSheetMatchComplete = (item?: TreeFileInfo | null) => {
- if (!isTreeSheetNode(item)) return false;
- const { requiredTotal, requiredMatched, matchedCount } = getTableRuleMatchStats(item?.tableRuleList);
- return isMatchSuccessful(requiredTotal, requiredMatched, matchedCount);
- };
- const getTreeFileName = (item?: TreeFileInfo | null) => item?.fileName || item?.filePath || '未命名文件';
- const getTreeSheetName = (item?: TreeFileInfo | null) => item?.sheetName || item?.fileName || '未命名工作表';
- const getTreeSheetTitle = (item?: TreeFileInfo | null) =>
- [getTreeSheetName(item), item?.tableName, getFileStatusLabel(item?.fileStatus)].filter(Boolean).join(' / ');
- const getFileNodeTitle = (item: TreeFileInfo) => {
- return isFileRootItem(item) ? getTreeFileName(item) : getTreeSheetName(item);
- };
- const toTreeNode = (item: TreeFileInfo): FileTreeNode => ({
- key: getTreeNodeKey(item),
- title: getFileNodeTitle(item),
- raw: item,
- children: [],
- selectable: !(isFileRootItem(item) && hasChildItems(item)),
- });
- const toTreeNodeDeep = (item: TreeFileInfo): FileTreeNode => {
- const children = (item.children || []).map((child) => toTreeNodeDeep(child));
- const node: FileTreeNode = {
- key: getTreeNodeKey(item),
- title: getFileNodeTitle(item),
- raw: item,
- children,
- selectable: !(isFileRootItem(item) && children.length > 0),
- };
- return node;
- };
- const buildTreeFromFlatList = (list: TreeFileInfo[]) => {
- const keyedList = list.map((item) => ({
- item,
- key: getTreeNodeKey(item),
- }));
- const nodeMap = new Map<string, FileTreeNode>();
- keyedList.forEach(({ item, key }) => {
- nodeMap.set(key, toTreeNode(item));
- });
- const roots: FileTreeNode[] = [];
- keyedList.forEach(({ item, key }) => {
- const node = nodeMap.get(key);
- if (!node) return;
- const parentKey = normalizeParentId(item.pid);
- if (!parentKey) {
- roots.push(node);
- return;
- }
- const parent = nodeMap.get(parentKey);
- if (!parent) {
- roots.push(node);
- return;
- }
- parent.children = parent.children || [];
- parent.children.push(node);
- parent.selectable = false;
- parent.title = getFileNodeTitle({
- ...parent.raw,
- children: parent.children.map((child) => child.raw!).filter(Boolean),
- } as TreeFileInfo);
- });
- return roots;
- };
- const sourceTree = computed<FileTreeNode[]>(() => {
- const list = fileList.value || [];
- if (!list.length) return [];
- // 若后端已返回树结构,则直接按 children 渲染
- const hasNestedChildren = list.some((item) => Array.isArray(item.children) && item.children.length > 0);
- if (hasNestedChildren) {
- return list.map((item) => toTreeNodeDeep(item));
- }
- // 否则按字符串 id/pid 组装树,兼容后端返回的大整数字符串
- return buildTreeFromFlatList(list);
- });
- const sortTreeNodes = (nodes: FileTreeNode[]) => {
- return [...nodes]
- .sort((a, b) => (sortMode.value === 'asc' ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title)))
- .map((node) => ({
- ...node,
- children: node.children?.length ? sortTreeNodes(node.children) : node.children,
- }));
- };
- const treeData = computed(() => {
- const list = sourceTree.value;
- if (!sortMode.value) return list;
- return sortTreeNodes(list);
- });
- const findTreeNodeByKey = (nodes: FileTreeNode[], key: string): FileTreeNode | null => {
- for (const node of nodes) {
- if (node.key === key) return node;
- if (node.children?.length) {
- const matched = findTreeNodeByKey(node.children, key);
- if (matched) return matched;
- }
- }
- return null;
- };
- const findFirstSelectableKey = (nodes: FileTreeNode[]): string => {
- for (const node of nodes) {
- if (node.selectable !== false) {
- return node.key;
- }
- if (node.children?.length) {
- const childKey = findFirstSelectableKey(node.children);
- if (childKey) return childKey;
- }
- }
- return '';
- };
- const resolveSelectableTreeKey = (key: string) => {
- const selectedNode = findTreeNodeByKey(treeData.value, key);
- if (!selectedNode) return '';
- if (selectedNode.selectable !== false) return selectedNode.key;
- return findFirstSelectableKey(selectedNode.children || []);
- };
- const iconMap = {
- unmatched: { key: 'unmatched', label: '未匹配(必选)', color: '#cc3333', icon: '●' },
- matched: { key: 'matched', label: '已匹配', color: '#4ba64b', icon: '●' },
- formatError: { key: 'formatError', label: '格式错误', color: '#f2a100', icon: '!' },
- } as const;
- const iconTips = [iconMap.unmatched, iconMap.matched, iconMap.formatError];
- const activeNodeKey = ref<string>('');
- const selectedTemplateId = ref<number>(0);
- const templateDetail = ref<TableInfo | null>(null);
- const templateDetailFromMock = ref(false);
- const mockTemplateMap = mockTemplateList.reduce(
- (acc, item) => {
- const templateId = normalizeTemplateId(item.id);
- if (templateId) {
- acc[templateId] = item;
- }
- return acc;
- },
- {} as Record<number, TableInfo>,
- );
- const fixedTargetFields = computed(() => {
- const template = templateDetail.value;
- const activeNode = activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
- const tableRules = normalizeTableRuleList(activeNode?.tableRuleList);
- if (!template && !selectedTemplateId.value && !tableRules.length) return [] as MappingTargetField[];
- const baseFields = tableRules.length
- ? buildTargetFieldsFromRuleList(tableRules)
- : buildTargetFieldsFromTemplateInfo(template);
- return applyTableRuleRequiredFlags(baseFields, tableRules);
- });
- const sortedMappingTargetFields = computed(() =>
- fixedTargetFields.value
- .map((item, index) => ({ item, index }))
- .sort((a, b) => {
- const aRequired = isMappingTargetRequired(a.item);
- const bRequired = isMappingTargetRequired(b.item);
- if (aRequired === bRequired) {
- return a.index - b.index;
- }
- return aRequired ? -1 : 1;
- })
- .map(({ item }) => item),
- );
- const defaultRightTableHeaders = ['列A', '列B', '列C', '列D', '列E', '列F', '列G', '列H'];
- const rightTableHeaders = ref([...defaultRightTableHeaders]);
- const addedColumns = ref<string[]>([]);
- const selectedAddedColumn = ref('');
- const sourceTableRows = ref<Record<string, string>[]>([]);
- const tablePreviewRows = ref<Record<string, string>[]>(JSON.parse(JSON.stringify(sourceTableRows.value)));
- const rulesList = [{ name: '单列清洗' }, { name: '其他清洗' }];
- type CleaningRuleKey = RulePipelineKey;
- interface CleaningRuleItem {
- name: string;
- key: CleaningRuleKey;
- show: boolean;
- active: boolean;
- }
- interface SelectedRuleItem {
- name: string;
- key: CleaningRuleKey;
- active: boolean;
- enabled: boolean;
- rules: Record<string, any>;
- }
- interface CleaningRuleExample {
- title: string;
- description: string;
- rows: Array<{
- label: string;
- value: string;
- }>;
- }
- interface PreviewColumnSnapshot {
- colIndex: number | null;
- selectedRowIndex: number | null;
- selectedAddedColumn: string;
- activeTargetKey: string;
- }
- type GroupingColumnDraft = {
- header: string;
- direction: string;
- count: number | undefined;
- title: string;
- };
- const ruleNameMap: Record<CleaningRuleKey, string> = {
- ignoreLine: '忽略行',
- dateConfig: '日期和时间',
- extractData: '提取数据',
- extractSegment: '分割提取',
- timestampConversion: '时间戳转换',
- removeSpecialCharacters: '移除特殊字符',
- absoluteValue: '绝对值',
- decimalConversion: '进制转换',
- replaceValue: '条件替换',
- groupingColumns: '组合列',
- magnificationAdjustment: '倍数放大器',
- yearComplete: '年份补全',
- usePreviousRowData: '沿用上一行数据',
- };
- const ruleExampleMap: Partial<Record<CleaningRuleKey, CleaningRuleExample>> = {
- ignoreLine: {
- title: '忽略行',
- description: '按行号跳过无需参与清洗和导入的数据行。',
- rows: [
- { label: '输入', value: '2,5,8' },
- { label: '效果', value: '第 2、5、8 行不会参与后续清洗与导入' },
- ],
- },
- dateConfig: {
- title: '日期和时间',
- description: '识别常见日期或时间文本,并统一转换成标准日期格式。',
- rows: [
- { label: '清洗前', value: '20260324153020' },
- { label: '清洗后', value: '2026-03-24 15:30:20' },
- ],
- },
- extractData: {
- title: '提取数据',
- description: '从字段值前后截取指定长度的内容。',
- rows: [
- { label: '清洗前', value: '13800138000' },
- { label: '前 3 位', value: '138' },
- { label: '后 4 位', value: '8000' },
- ],
- },
- extractSegment: {
- title: '分割提取',
- description: '按分隔符拆分字段值,再提取指定段位。',
- rows: [
- { label: '清洗前', value: '四川省-成都市-高新区' },
- { label: '分隔符', value: '-' },
- { label: '清洗后', value: '成都市' },
- ],
- },
- timestampConversion: {
- title: '时间戳转换',
- description: '自动识别秒级或毫秒级时间戳,并转换为日期时间。',
- rows: [
- { label: '清洗前', value: '1711209600' },
- { label: '清洗后', value: '2024-03-24 00:00:00' },
- ],
- },
- removeSpecialCharacters: {
- title: '移除特殊字符',
- description: '移除空格、换行或自定义特殊字符。',
- rows: [
- { label: '清洗前', value: '138 0013-8000' },
- { label: '清洗后', value: '13800138000' },
- ],
- },
- absoluteValue: {
- title: '绝对值',
- description: '将数值型字段统一转换为正数。',
- rows: [
- { label: '清洗前', value: '-20' },
- { label: '清洗后', value: '20' },
- ],
- },
- decimalConversion: {
- title: '进制转换',
- description: '按配置将数值从当前进制转换到目标进制。',
- rows: [
- { label: '清洗前', value: '56(10进制)' },
- { label: '清洗后', value: '38(16进制)' },
- ],
- },
- replaceValue: {
- title: '条件替换',
- description: '按包含或等于条件,将命中的文本替换为目标值。',
- rows: [
- { label: '包含', value: '未知开户地址 -> --开户地址' },
- { label: '等于', value: '空 -> --' },
- ],
- },
- groupingColumns: {
- title: '组合列',
- description: '将多个来源列按顺序截取并拼接为新列。',
- rows: [
- { label: '配置', value: '姓名前 1 位 + 手机号后 4 位' },
- { label: '清洗后', value: '张2369' },
- ],
- },
- magnificationAdjustment: {
- title: '倍数放大器',
- description: '将数值按配置倍数放大,适合金额或比例字段换算。',
- rows: [
- { label: '清洗前', value: '1.23' },
- { label: '倍数', value: '100' },
- { label: '清洗后', value: '123' },
- ],
- },
- yearComplete: {
- title: '年份补全',
- description: '当日期缺少年份时,按当前年份自动补全。',
- rows: [
- { label: '清洗前', value: '02-23 12:00:00' },
- { label: '清洗后', value: '2026-02-23 12:00:00' },
- ],
- },
- };
- const disabledCleaningRuleKeySet = new Set<CleaningRuleKey>([
- 'groupingColumns',
- 'yearComplete',
- 'removeSpecialCharacters',
- 'usePreviousRowData',
- ]);
- const isCleaningRuleEnabledInUi = (key?: CleaningRuleKey | string | null): key is CleaningRuleKey =>
- !!key && !disabledCleaningRuleKeySet.has(key as CleaningRuleKey);
- const sanitizeSelectedRuleList = (rules?: SelectedRuleItem[] | null) =>
- (Array.isArray(rules) ? rules : []).filter((rule) => isCleaningRuleEnabledInUi(rule?.key));
- const sanitizeSelectedRuleGroups = (groups?: SelectedRuleItem[][] | null) =>
- (Array.isArray(groups) ? groups : []).map((rules) => sanitizeSelectedRuleList(rules));
- const cleaningRulesList = ref<CleaningRuleItem[]>([
- { name: '忽略行', key: 'ignoreLine', show: false, active: false },
- { name: '日期和时间', key: 'dateConfig', show: true, active: false },
- { name: '提取数据', key: 'extractData', show: true, active: false },
- { name: '分割提取', key: 'extractSegment', show: true, active: false },
- { name: '时间戳转换', key: 'timestampConversion', show: true, active: false },
- { name: '绝对值', key: 'absoluteValue', show: true, active: false },
- { name: '进制转换', key: 'decimalConversion', show: true, active: false },
- { name: '条件替换', key: 'replaceValue', show: true, active: false },
- { name: '倍数放大器', key: 'magnificationAdjustment', show: true, active: false },
- ]);
- const activeRules = ref(0);
- const isSharedRuleMode = computed(() => activeRules.value === 1);
- const currentColumn = ref<number | null>(null);
- const activeMappingTargetKey = ref('');
- const selectedRowIndex = ref<number | null>(null);
- const headerRowIndex = ref<number | null>(null);
- const headerRowInput = ref('');
- const tableMockRef = ref<HTMLElement | null>(null);
- const generatedColumnHeaderRowMap = ref<Record<string, number | null>>({});
- const selectRules = ref<SelectedRuleItem[][]>([]);
- const selectedRules = ref<SelectedRuleItem[]>([]);
- const currentRuleTitle = ref('');
- const omitLineInput = ref('');
- const dateConfigType = ref(1);
- const extractDataMode = ref<'before' | 'after'>('before');
- const extractDataBefore = ref('');
- const extractDataAfter = ref('');
- const extractSegmentValue = ref('');
- const extractSegmentRow = ref<number | undefined>(1);
- const absoluteValueChecked = ref(false);
- const decimalEnabled = ref(false);
- const decimalSourceBase = ref('10进制');
- const decimalTargetBase = ref('16进制');
- const replaceEnabled = ref(true);
- const replaceMode = ref(0);
- const replaceSource = ref('');
- const replaceTarget = ref('');
- const removeSpaceChecked = ref(false);
- const removeTabChecked = ref(false);
- const removeCustomChecked = ref(false);
- const removeCustomText = ref('');
- const groupingEnabled = ref(false);
- const groupingColumnsDraft = ref<GroupingColumnDraft[]>(normalizeGroupingColumnsDraft());
- const magnificationValue = ref<number | undefined>(1);
- const syncExtractDataMode = () => {
- extractDataMode.value = extractDataAfter.value && !extractDataBefore.value ? 'after' : 'before';
- };
- const handleExtractDataModeChange = (mode: 'before' | 'after') => {
- extractDataMode.value = mode;
- if (mode === 'before') {
- extractDataAfter.value = '';
- return;
- }
- extractDataBefore.value = '';
- };
- const dateTypeOptions = [
- { label: '日期和时间', value: 1 },
- { label: '日期', value: 2 },
- { label: '时间', value: 3 },
- ];
- const decimalBaseOptions = ['2进制', '8进制', '10进制', '16进制'].map((item) => ({
- label: item,
- value: item,
- }));
- const replaceModeOptions = [
- { label: '包含', value: 0 },
- { label: '等于', value: 1 },
- ];
- function createGroupingColumnDraft(item?: Partial<GroupingColumnDraft> | Record<string, any>): GroupingColumnDraft {
- const header = String(item?.header ?? '').trim();
- const rawCount = item?.count;
- const count =
- rawCount === null || rawCount === undefined || rawCount === ''
- ? undefined
- : Number.isFinite(Number(rawCount))
- ? Number(rawCount)
- : undefined;
- return {
- header,
- direction: String(item?.direction ?? '').trim(),
- count,
- title: String(item?.title ?? header).trim(),
- };
- }
- function normalizeGroupingColumnsDraft(
- list?: Array<Partial<GroupingColumnDraft> | Record<string, any>> | null,
- ): GroupingColumnDraft[] {
- const normalized = Array.isArray(list) ? list.map((item) => createGroupingColumnDraft(item)) : [];
- return normalized.length ? normalized : [createGroupingColumnDraft()];
- }
- type NodeCacheState = {
- selectedTemplateId: number;
- fieldMapping: Record<string, string>;
- rightTableHeaders: string[];
- addedColumns: string[];
- selectedAddedColumn: string;
- sourceTableRows: Record<string, string>[];
- selectRules: SelectedRuleItem[][];
- selectedRules: SelectedRuleItem[];
- activeRules: number;
- currentColumn: number | null;
- selectedRowIndex: number | null;
- headerRowIndex: number | null;
- headerRowInput: string;
- currentRuleTitle: string;
- omitLineInput: string;
- dateConfigType: number;
- extractDataBefore: string;
- extractDataAfter: string;
- extractSegmentValue: string;
- extractSegmentRow: number | undefined;
- absoluteValueChecked: boolean;
- decimalEnabled: boolean;
- decimalSourceBase: string;
- decimalTargetBase: string;
- replaceEnabled: boolean;
- replaceMode: number;
- replaceSource: string;
- replaceTarget: string;
- removeSpaceChecked: boolean;
- removeTabChecked: boolean;
- removeCustomChecked: boolean;
- removeCustomText: string;
- groupingEnabled: boolean;
- groupingColumnsDraft: GroupingColumnDraft[];
- magnificationValue: number | undefined;
- addedColumnConfig: Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>;
- splitColumnConfig: Record<
- string,
- { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
- >;
- generatedColumnHeaderRowMap: Record<string, number | null>;
- recognitionDirectionFields: RecognitionDirectionField[];
- recognitionBasicInfoBaseFuncRegx: string;
- recognitionBasicInfoManualVisible: boolean;
- recognitionBasicInfoExpanded: boolean;
- recognitionBasicInfoRows: RecognitionBasicInfoRow[];
- recognitionAmountZfEnabled: boolean;
- childTemplateName: string;
- };
- const MAX_NODE_CACHE_SIZE = 20;
- const nodeStateCache = new Map<string, NodeCacheState>();
- const initialNodeStatusSignatureMap = new Map<string, string>();
- const analyzingNodeKeys = ref<string[]>([]);
- const cloneDeepSafe = <T,>(val: T): T => JSON.parse(JSON.stringify(val));
- const normalizeGeneratedColumnHeaderRowIndex = (value: unknown, rowCount: number) => {
- const rowIndex = Number(value);
- return Number.isInteger(rowIndex) && rowIndex >= 0 && rowIndex < rowCount ? rowIndex : null;
- };
- const buildGeneratedColumnHeaderRowMap = (
- headers: string[] = addedColumns.value,
- rows: Record<string, string>[] = sourceTableRows.value,
- currentMap?: Record<string, number | null> | null,
- fallbackRowIndex: number | null = headerRowIndex.value,
- ) =>
- headers.reduce(
- (acc, header) => {
- if (!header) return acc;
- if (currentMap && Object.prototype.hasOwnProperty.call(currentMap, header)) {
- acc[header] = normalizeGeneratedColumnHeaderRowIndex(currentMap[header], rows.length);
- return acc;
- }
- const inferredRowIndex = rows.findIndex((row) => String(row?.[header] ?? '') === header);
- acc[header] =
- inferredRowIndex >= 0
- ? inferredRowIndex
- : normalizeGeneratedColumnHeaderRowIndex(fallbackRowIndex, rows.length);
- return acc;
- },
- {} as Record<string, number | null>,
- );
- const getGeneratedColumnHeaderRowIndex = (columnName: string) =>
- buildGeneratedColumnHeaderRowMap(
- addedColumns.value.includes(columnName) ? [columnName] : [],
- sourceTableRows.value,
- generatedColumnHeaderRowMap.value,
- )[columnName] ?? null;
- const setNodeAnalyzing = (nodeKey: string, analyzing: boolean) => {
- if (!nodeKey) return;
- if (analyzing) {
- if (!analyzingNodeKeys.value.includes(nodeKey)) {
- analyzingNodeKeys.value = [...analyzingNodeKeys.value, nodeKey];
- }
- return;
- }
- if (analyzingNodeKeys.value.includes(nodeKey)) {
- analyzingNodeKeys.value = analyzingNodeKeys.value.filter((item) => item !== nodeKey);
- }
- };
- const touchNodeCache = (nodeKey: string, state: NodeCacheState) => {
- if (nodeStateCache.has(nodeKey)) {
- nodeStateCache.delete(nodeKey);
- }
- nodeStateCache.set(nodeKey, state);
- if (nodeStateCache.size > MAX_NODE_CACHE_SIZE) {
- const oldestKey = nodeStateCache.keys().next().value;
- if (oldestKey) nodeStateCache.delete(oldestKey);
- }
- };
- const buildNodeCacheState = (): NodeCacheState => ({
- selectedTemplateId: selectedTemplateId.value,
- fieldMapping: cloneDeepSafe(fieldMapping.value),
- rightTableHeaders: [...rightTableHeaders.value],
- addedColumns: [...addedColumns.value],
- selectedAddedColumn: selectedAddedColumn.value,
- sourceTableRows: cloneDeepSafe(sourceTableRows.value),
- selectRules: sanitizeSelectedRuleGroups(cloneDeepSafe(selectRules.value)),
- selectedRules: sanitizeSelectedRuleList(cloneDeepSafe(selectedRules.value)),
- activeRules: activeRules.value,
- currentColumn: currentColumn.value,
- selectedRowIndex: selectedRowIndex.value,
- headerRowIndex: headerRowIndex.value,
- headerRowInput: headerRowInput.value,
- currentRuleTitle: currentRuleTitle.value,
- omitLineInput: omitLineInput.value,
- dateConfigType: dateConfigType.value,
- extractDataBefore: extractDataBefore.value,
- extractDataAfter: extractDataAfter.value,
- extractSegmentValue: extractSegmentValue.value,
- extractSegmentRow: extractSegmentRow.value,
- absoluteValueChecked: absoluteValueChecked.value,
- decimalEnabled: decimalEnabled.value,
- decimalSourceBase: decimalSourceBase.value,
- decimalTargetBase: decimalTargetBase.value,
- replaceEnabled: replaceEnabled.value,
- replaceMode: replaceMode.value,
- replaceSource: replaceSource.value,
- replaceTarget: replaceTarget.value,
- removeSpaceChecked: removeSpaceChecked.value,
- removeTabChecked: removeTabChecked.value,
- removeCustomChecked: removeCustomChecked.value,
- removeCustomText: removeCustomText.value,
- groupingEnabled: groupingEnabled.value,
- groupingColumnsDraft: cloneDeepSafe(groupingColumnsDraft.value),
- magnificationValue: magnificationValue.value,
- addedColumnConfig: cloneDeepSafe(addedColumnConfig.value),
- splitColumnConfig: cloneDeepSafe(splitColumnConfig.value),
- generatedColumnHeaderRowMap: cloneDeepSafe(buildGeneratedColumnHeaderRowMap()),
- recognitionDirectionFields: cloneDeepSafe(recognitionDirectionFields.value),
- recognitionBasicInfoBaseFuncRegx: recognitionBasicInfoBaseFuncRegx.value,
- recognitionBasicInfoManualVisible: recognitionBasicInfoManualVisible.value,
- recognitionBasicInfoExpanded: recognitionBasicInfoExpanded.value,
- recognitionBasicInfoRows: cloneDeepSafe(recognitionBasicInfoRows.value),
- recognitionAmountZfEnabled: recognitionAmountZfEnabled.value,
- childTemplateName: childTemplateName.value,
- });
- const buildNodeStatusSignature = (state: NodeCacheState) =>
- JSON.stringify({
- selectedTemplateId: state.selectedTemplateId,
- fieldMapping: state.fieldMapping,
- rightTableHeaders: state.rightTableHeaders,
- addedColumns: state.addedColumns,
- headerRowIndex: state.headerRowIndex,
- selectRules: state.selectRules.map((rules) =>
- rules.map((rule) => ({
- key: rule.key,
- name: rule.name,
- enabled: rule.enabled,
- rules: rule.rules,
- })),
- ),
- selectedRules: state.selectedRules.map((rule) => ({
- key: rule.key,
- name: rule.name,
- enabled: rule.enabled,
- rules: rule.rules,
- })),
- addedColumnConfig: state.addedColumnConfig,
- splitColumnConfig: state.splitColumnConfig,
- generatedColumnHeaderRowMap: state.generatedColumnHeaderRowMap,
- recognitionDirectionFields: (state.recognitionDirectionFields || []).map((item) => ({
- key: item.key,
- label: item.label,
- tab: item.tab,
- entries: item.entries.map((entry) => ({
- label: entry.label,
- value: entry.value,
- })),
- })),
- recognitionBasicInfoBaseFuncRegx: state.recognitionBasicInfoBaseFuncRegx,
- recognitionBasicInfoManualVisible: state.recognitionBasicInfoManualVisible,
- recognitionBasicInfoRows: state.recognitionBasicInfoRows.map((item) => ({
- headerMatch: item.headerMatch,
- targetField: item.targetField,
- endFlag: item.endFlag,
- })),
- recognitionAmountZfEnabled: state.recognitionAmountZfEnabled,
- childTemplateName: state.childTemplateName,
- });
- const applyNodeCacheState = (state: NodeCacheState) => {
- selectedTemplateId.value = state.selectedTemplateId;
- fieldMapping.value = cloneDeepSafe(state.fieldMapping);
- rightTableHeaders.value = [...state.rightTableHeaders];
- addedColumns.value = [...state.addedColumns];
- selectedAddedColumn.value = state.selectedAddedColumn;
- sourceTableRows.value = cloneDeepSafe(state.sourceTableRows);
- selectRules.value = sanitizeSelectedRuleGroups(cloneDeepSafe(state.selectRules));
- selectedRules.value = sanitizeSelectedRuleList(cloneDeepSafe(state.selectedRules));
- activeRules.value = state.activeRules;
- currentColumn.value = state.currentColumn;
- selectedRowIndex.value = state.selectedRowIndex;
- headerRowIndex.value = state.headerRowIndex;
- headerRowInput.value = state.headerRowInput;
- currentRuleTitle.value = state.currentRuleTitle;
- omitLineInput.value = state.omitLineInput;
- dateConfigType.value = state.dateConfigType;
- extractDataBefore.value = state.extractDataBefore;
- extractDataAfter.value = state.extractDataAfter;
- syncExtractDataMode();
- extractSegmentValue.value = state.extractSegmentValue;
- extractSegmentRow.value = state.extractSegmentRow;
- absoluteValueChecked.value = state.absoluteValueChecked;
- decimalEnabled.value = state.decimalEnabled;
- decimalSourceBase.value = state.decimalSourceBase;
- decimalTargetBase.value = state.decimalTargetBase;
- replaceEnabled.value = state.replaceEnabled;
- replaceMode.value = state.replaceMode;
- replaceSource.value = state.replaceSource;
- replaceTarget.value = state.replaceTarget;
- removeSpaceChecked.value = state.removeSpaceChecked;
- removeTabChecked.value = state.removeTabChecked;
- removeCustomChecked.value = state.removeCustomChecked;
- removeCustomText.value = state.removeCustomText;
- groupingEnabled.value = state.groupingEnabled;
- groupingColumnsDraft.value = normalizeGroupingColumnsDraft(cloneDeepSafe(state.groupingColumnsDraft));
- magnificationValue.value = state.magnificationValue;
- addedColumnConfig.value = cloneDeepSafe(state.addedColumnConfig);
- splitColumnConfig.value = cloneDeepSafe(state.splitColumnConfig);
- generatedColumnHeaderRowMap.value = buildGeneratedColumnHeaderRowMap(
- state.addedColumns,
- state.sourceTableRows,
- state.generatedColumnHeaderRowMap,
- state.headerRowIndex,
- );
- recognitionDirectionFields.value = cloneDeepSafe(state.recognitionDirectionFields || []);
- recognitionBasicInfoBaseFuncRegx.value = state.recognitionBasicInfoBaseFuncRegx;
- recognitionBasicInfoManualVisible.value = state.recognitionBasicInfoManualVisible;
- recognitionBasicInfoExpanded.value = state.recognitionBasicInfoExpanded;
- recognitionBasicInfoRows.value = cloneDeepSafe(state.recognitionBasicInfoRows);
- recognitionAmountZfEnabled.value = !!state.recognitionAmountZfEnabled;
- childTemplateName.value = state.childTemplateName;
- if (!selectedRules.value.some((item) => item.active) && currentColumn.value !== null) {
- const currentColumnRules = selectRules.value[currentColumn.value] || [];
- if (!currentColumnRules.some((item) => item.active)) {
- currentRuleTitle.value = '';
- }
- }
- runRulePipeline();
- };
- const resetNodeWorkState = () => {
- selectedTemplateId.value = 0;
- templateDetail.value = null;
- templateDetailFromMock.value = false;
- activeTemplateItem.value = null;
- fieldMapping.value = {};
- activeMappingTargetKey.value = '';
- rightTableHeaders.value = [];
- addedColumns.value = [];
- selectedAddedColumn.value = '';
- sourceTableRows.value = [];
- selectRules.value = [];
- selectedRules.value = [];
- activeRules.value = 0;
- currentColumn.value = null;
- selectedRowIndex.value = null;
- headerRowIndex.value = null;
- headerRowInput.value = '';
- currentRuleTitle.value = '';
- omitLineInput.value = '';
- dateConfigType.value = 1;
- extractDataMode.value = 'before';
- extractDataBefore.value = '';
- extractDataAfter.value = '';
- extractSegmentValue.value = '';
- extractSegmentRow.value = 1;
- absoluteValueChecked.value = false;
- decimalEnabled.value = false;
- decimalSourceBase.value = '10进制';
- decimalTargetBase.value = '16进制';
- replaceEnabled.value = true;
- replaceMode.value = 0;
- replaceSource.value = '';
- replaceTarget.value = '';
- removeSpaceChecked.value = false;
- removeTabChecked.value = false;
- removeCustomChecked.value = false;
- removeCustomText.value = '';
- groupingEnabled.value = false;
- groupingColumnsDraft.value = normalizeGroupingColumnsDraft();
- magnificationValue.value = 1;
- addedColumnConfig.value = {};
- splitColumnConfig.value = {};
- generatedColumnHeaderRowMap.value = {};
- recognitionDirectionFields.value = [];
- recognitionBasicInfoBaseFuncRegx.value = '';
- recognitionBasicInfoManualVisible.value = false;
- recognitionBasicInfoExpanded.value = false;
- recognitionBasicInfoRows.value = [];
- recognitionAmountZfEnabled.value = false;
- childTemplateName.value = '';
- runRulePipeline();
- };
- const clearActiveNodeTemplateState = () => {
- const node = activeNodeInfo.value;
- const key = activeNodeKey.value;
- if (!node || !key) return;
- node.templateId = null;
- node.mainId = null;
- node.tableName = null;
- node.tableRuleList = [];
- node.childTemplateName = null;
- node.funcRegx = null;
- nodeStateCache.delete(key);
- };
- const saveCurrentNodeState = () => {
- const key = activeNodeKey.value;
- if (!key) return;
- syncActiveNodeStructure();
- touchNodeCache(key, buildNodeCacheState());
- };
- const switchNodeState = async (nextKey: string) => {
- if (!nextKey) return;
- if (activeNodeKey.value === nextKey) return;
- saveCurrentNodeState();
- setNodeAnalyzing(nextKey, true);
- activeNodeKey.value = nextKey;
- const currentNode = findTreeNodeByKey(treeData.value, nextKey)?.raw || null;
- structureSyncSuspended.value = true;
- try {
- const cached = nodeStateCache.get(nextKey);
- if (cached) {
- applyNodeCacheState(cached);
- await loadTemplateDetail(cached.selectedTemplateId, { silent: true, requestDetail: false });
- touchNodeCache(nextKey, cached);
- return;
- }
- resetNodeWorkState();
- const shouldLoadNodeTemplate = !isTemplateMatchedFailStatus(currentNode?.fileStatus);
- await loadTemplateDetail(shouldLoadNodeTemplate ? currentNode?.templateId : undefined, {
- silent: true,
- requestDetail: false,
- });
- await applyNodePreset(currentNode);
- const initialState = buildNodeCacheState();
- touchNodeCache(nextKey, initialState);
- if (!initialNodeStatusSignatureMap.has(nextKey)) {
- initialNodeStatusSignatureMap.set(nextKey, buildNodeStatusSignature(initialState));
- }
- } finally {
- structureSyncSuspended.value = false;
- syncActiveNodeStructure();
- setNodeAnalyzing(nextKey, false);
- }
- };
- const defaultRuleConfig = (): Record<string, any> => ({
- segment: '',
- segmentRow: 1,
- beforeModel: '',
- nextModel: '',
- timeStamp: false,
- dateType: 1,
- specialList: [],
- specialStr: '',
- absoluteData: false,
- decimalData: false,
- decimalModel: '16进制',
- decimalModel2: '10进制',
- replaceFlag: true,
- sourceData: '',
- targetData: '',
- replaceModel: 0,
- omitLine: '',
- groupFlag: false,
- groupColumns: normalizeGroupingColumnsDraft(),
- magnificationAdjustmentValue: 1,
- });
- const structureSyncSuspended = ref(false);
- const normalizeStoredLineNoToRowIndex = (value?: string | number | null) => {
- const lineNo = Number(value);
- if (!Number.isInteger(lineNo)) return null;
- return lineNo <= 0 ? 0 : lineNo - 1;
- };
- const buildStoredLineNo = (rowIndex: number | null) => {
- if (rowIndex === null || rowIndex < 0) return null;
- return rowIndex + 1;
- };
- const toRuleBoolean = (value: unknown, defaultValue = false) => {
- if (value === null || value === undefined || value === '') {
- return defaultValue;
- }
- if (typeof value === 'string') {
- return value !== 'false';
- }
- return Boolean(value);
- };
- const persistFileListToSessionStorage = () => {
- try {
- sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizeFileInfoTree(fileList.value)));
- } catch (error) {
- // 忽略序列化异常,避免影响当前清洗编辑流程。
- }
- };
- const createPreviewRowsByHeaders = (headers: string[]) => {
- return headers.length ? [] : [];
- };
- const applyPreviewDataset = (dataset: PreviewFileDataset) => {
- rightTableHeaders.value = [...dataset.headers];
- addedColumns.value = [];
- generatedColumnHeaderRowMap.value = {};
- selectedAddedColumn.value = '';
- sourceTableRows.value = dataset.rows.length ? dataset.rows : [];
- tablePreviewRows.value = cloneDeepSafe(sourceTableRows.value);
- return true;
- };
- type PreviewStateSnapshot = PreviewFileDataset & {
- headerRowIndex: number | null;
- selectedRowIndex: number | null;
- };
- const pickPreviewRowByHeaders = (row: Record<string, string>, headers: string[]) =>
- headers.reduce(
- (acc, header) => {
- acc[header] = String(row?.[header] ?? '');
- return acc;
- },
- {} as Record<string, string>,
- );
- const captureSourcePreviewSnapshot = (): PreviewStateSnapshot | null => {
- const addedHeaderSet = new Set(addedColumns.value);
- const headers = rightTableHeaders.value.filter((header) => !addedHeaderSet.has(header));
- if (!headers.length) return null;
- return {
- headers,
- rows: sourceTableRows.value.map((row) => pickPreviewRowByHeaders(row, headers)),
- headerRowIndex: headerRowIndex.value,
- selectedRowIndex: selectedRowIndex.value,
- };
- };
- const restorePreviewSnapshot = (snapshot: PreviewStateSnapshot) => {
- applyPreviewDataset({
- headers: snapshot.headers,
- rows: snapshot.rows,
- });
- headerRowIndex.value = snapshot.headerRowIndex;
- selectedRowIndex.value = snapshot.selectedRowIndex;
- headerRowInput.value = snapshot.headerRowIndex === null ? '' : String(snapshot.headerRowIndex + 1);
- };
- const loadPreviewDatasetFromFile = async (item?: TreeFileInfo | null): Promise<PreviewFileDataset | null> => {
- // 从 DuckDB 动态表 raw_{sheetId} 查询预览数据(sheet 节点 id 即 sheetId),
- // 替代原 Electron 端本地文件读取
- if (!isTreeSheetNode(item) || item?.id === null || item?.id === undefined) return null;
- try {
- const payload = (await rawPreview({
- sheetId: item.id,
- headers: normalizeTemplateHeaders(item?.header),
- headerRowIndex: normalizeStoredLineNoToRowIndex(item?.lineNo),
- limit: 2000,
- })) as PreviewFileDataset | null;
- if (!payload?.headers?.length) return null;
- return {
- headers: payload.headers.map((header) => normalizeRuleText(header)).filter(Boolean),
- rows: Array.isArray(payload.rows) ? payload.rows : [],
- };
- } catch (error) {
- console.error('读取预览数据失败', item?.id, error);
- return null;
- }
- };
- const applyPresetHeadersFromNode = (item?: TreeFileInfo | null) => {
- const headers = normalizeTemplateHeaders(item?.header);
- if (!headers.length) return false;
- return applyPreviewDataset({
- headers,
- rows: [],
- });
- };
- const insertHeaderAt = (header: string, targetIndex?: number | null) => {
- if (!header) return;
- if (!rightTableHeaders.value.includes(header)) {
- const maxIndex = rightTableHeaders.value.length;
- const nextIndex =
- typeof targetIndex === 'number' && Number.isInteger(targetIndex)
- ? Math.min(Math.max(targetIndex, 0), maxIndex)
- : maxIndex;
- rightTableHeaders.value.splice(nextIndex, 0, header);
- }
- if (!addedColumns.value.includes(header)) {
- addedColumns.value.push(header);
- }
- if (!Object.prototype.hasOwnProperty.call(generatedColumnHeaderRowMap.value, header)) {
- generatedColumnHeaderRowMap.value[header] = headerRowIndex.value;
- }
- sourceTableRows.value = sourceTableRows.value.map((row) => ({
- ...row,
- [header]: String(row[header] ?? ''),
- }));
- };
- const resolveStoredRuleKey = (rule?: TreeStoredFunRule | null) => {
- const directKey = normalizeRuleText(rule?.ruleKey) as CleaningRuleKey;
- if (
- directKey &&
- Object.prototype.hasOwnProperty.call(ruleNameMap, directKey) &&
- isCleaningRuleEnabledInUi(directKey)
- ) {
- return directKey;
- }
- const legacyKey = normalizeRuleText(rule?.key) as CleaningRuleKey;
- if (
- legacyKey &&
- Object.prototype.hasOwnProperty.call(ruleNameMap, legacyKey) &&
- isCleaningRuleEnabledInUi(legacyKey)
- ) {
- return legacyKey;
- }
- const className = normalizeRuleText(rule?.['@class']);
- if (className.endsWith('FunReplace')) return 'replaceValue';
- if (className.endsWith('FunMultiplier')) return 'magnificationAdjustment';
- if (className.endsWith('FunAbs')) return 'absoluteValue';
- if (className.endsWith('FunRadix')) return 'decimalConversion';
- if (className.endsWith('FunSpExtra')) return 'extractSegment';
- if (className.endsWith('FunExtra')) return 'extractData';
- if (className.endsWith('FunDateTime'))
- return legacyKey === 'timestampConversion' ? 'timestampConversion' : 'dateConfig';
- if (className.endsWith('FunDate') || className.endsWith('FunTime')) return 'dateConfig';
- if (className.endsWith('FunRegular')) return 'removeSpecialCharacters';
- return null;
- };
- const deserializeStoredRule = (rule?: TreeStoredFunRule | null): SelectedRuleItem | null => {
- const key = resolveStoredRuleKey(rule);
- if (!key) return null;
- const rules = {
- ...defaultRuleConfig(),
- ...(cloneDeepSafe(rule?.rules || {}) as Record<string, any>),
- };
- const className = normalizeRuleText(rule?.['@class']);
- if (key === 'replaceValue') {
- rules.sourceData = String(rule?.target ?? rules.sourceData ?? '');
- rules.targetData = String(rule?.replacement ?? rules.targetData ?? '');
- }
- if (key === 'magnificationAdjustment') {
- rules.magnificationAdjustmentValue = Number(rule?.factor ?? rules.magnificationAdjustmentValue ?? 1) || 1;
- }
- if (key === 'extractSegment') {
- rules.segment = String(rule?.key ?? rules.segment ?? '');
- rules.segmentRow = Number(rule?.index ?? rules.segmentRow ?? 1) || 1;
- }
- if (key === 'extractData') {
- const len = Number(rule?.len ?? 0) || 0;
- const direction = normalizeRuleText(rule?.metaDirection) || (rule?.opened === false ? 'after' : 'before');
- if (direction === 'after') {
- rules.nextModel = len ? String(len) : rules.nextModel;
- } else {
- rules.beforeModel = len ? String(len) : rules.beforeModel;
- }
- }
- if (key === 'decimalConversion') {
- if (!rules.decimalModel && !rules.decimalModel2) {
- if (Number(rule?.radixType) === 0) {
- rules.decimalModel = '16进制';
- rules.decimalModel2 = '10进制';
- }
- if (Number(rule?.radixType) === 1) {
- rules.decimalModel = '10进制';
- rules.decimalModel2 = '16进制';
- }
- }
- rules.decimalData = rule?.opened ?? rules.decimalData ?? false;
- }
- if (key === 'absoluteValue') {
- rules.absoluteData = rule?.opened ?? rules.absoluteData ?? true;
- }
- if (key === 'dateConfig') {
- if (className.endsWith('FunDate')) {
- rules.dateType = 2;
- } else if (className.endsWith('FunTime')) {
- rules.dateType = 3;
- } else {
- rules.dateType = 1;
- }
- }
- if (key === 'timestampConversion') {
- rules.timeStamp = true;
- }
- if (key === 'removeSpecialCharacters') {
- const regex = String(rule?.regex ?? '');
- if (!Array.isArray(rules.specialList) || !rules.specialList.length) {
- const parsedRegularRule = parseRemoveSpecialRegex(regex);
- rules.specialList = parsedRegularRule.specialList;
- rules.specialStr = parsedRegularRule.specialStr;
- }
- }
- return {
- key,
- name: normalizeRuleText(rule?.name) || ruleNameMap[key],
- active: false,
- enabled: true,
- rules,
- };
- };
- const buildStoredFunRules = (rule: SelectedRuleItem): TreeStoredFunRule[] => {
- const rules = cloneDeepSafe(rule.rules || {});
- const fallbackRule = {
- ruleKey: rule.key,
- rules,
- };
- if (rule.key === 'replaceValue') {
- const fun = new FunReplace();
- fun.target = String(rules.sourceData || '');
- fun.replacement = String(rules.targetData || '');
- return [buildStoredFunPayload(fun)];
- }
- if (rule.key === 'extractSegment') {
- const fun = new FunSpExtra();
- fun.key = String(rules.segment || '');
- fun.index = Number(rules.segmentRow || 1) || 1;
- return [buildStoredFunPayload(fun)];
- }
- if (rule.key === 'extractData') {
- const nextRules: TreeStoredFunRule[] = [];
- const beforeLen = Number(rules.beforeModel || 0) || 0;
- const afterLen = Number(rules.nextModel || 0) || 0;
- if (beforeLen > 0) {
- const fun = new FunExtra();
- fun.opened = true;
- fun.len = beforeLen;
- nextRules.push(buildStoredFunPayload(fun));
- }
- if (afterLen > 0) {
- const fun = new FunExtra();
- fun.opened = false;
- fun.len = afterLen;
- nextRules.push(buildStoredFunPayload(fun));
- }
- return nextRules.length ? nextRules : [fallbackRule];
- }
- if (rule.key === 'timestampConversion') {
- const fun = new FunDateTime();
- return [buildStoredFunPayload(fun, { ruleKey: 'timestampConversion' })];
- }
- if (rule.key === 'dateConfig') {
- const dateType = Number(rules.dateType || 1) || 1;
- const fun = dateType === 2 ? new FunDate() : dateType === 3 ? new FunTime() : new FunDateTime();
- return [buildStoredFunPayload(fun)];
- }
- if (rule.key === 'removeSpecialCharacters') {
- const fun = new FunRegular();
- fun.regex = buildRemoveSpecialRegex(
- Array.isArray(rules.specialList) ? rules.specialList : [],
- String(rules.specialStr || ''),
- );
- return [buildStoredFunPayload(fun)];
- }
- if (rule.key === 'magnificationAdjustment') {
- const fun = new FunMultiplier();
- fun.factor = Number(rules.magnificationAdjustmentValue || 1) || 1;
- return [buildStoredFunPayload(fun)];
- }
- if (rule.key === 'absoluteValue') {
- const fun = new FunAbs();
- fun.opened = toRuleBoolean(rules.absoluteData, true);
- return [buildStoredFunPayload(fun)];
- }
- if (rule.key === 'decimalConversion') {
- const targetBase = normalizeDecimalBaseLabel(rules.decimalModel);
- const currentBase = normalizeDecimalBaseLabel(rules.decimalModel2);
- if (currentBase === '10进制' && targetBase === '16进制') {
- const fun = new FunRadix();
- fun.radixType = 0;
- fun.opened = toRuleBoolean(rules.decimalData, false);
- return [buildStoredFunPayload(fun)];
- }
- if (currentBase === '16进制' && targetBase === '10进制') {
- const fun = new FunRadix();
- fun.radixType = 1;
- fun.opened = toRuleBoolean(rules.decimalData, false);
- return [buildStoredFunPayload(fun)];
- }
- return [fallbackRule];
- }
- return [fallbackRule];
- };
- const serializeStoredFunRules = (rulesList: SelectedRuleItem[], startSort = 0) => {
- let currentSort = startSort;
- return rulesList
- .filter((rule) => rule.enabled && isCleaningRuleEnabledInUi(rule.key))
- .flatMap((rule) =>
- buildStoredFunRules(rule).map((item) => ({
- ...item,
- sort: ++currentSort,
- })),
- );
- };
- const sanitizeStoredFunRulePayload = (
- rule?: TreeStoredFunRule | null,
- options?: { keepRuleKey?: boolean },
- ): TreeStoredFunRule | null => {
- if (!rule) return null;
- const className = normalizeRuleText(rule?.['@class']);
- const sort = Number(rule?.sort);
- const withSort = (payload: TreeStoredFunRule) =>
- Number.isInteger(sort) && sort > 0 ? { ...payload, sort } : payload;
- const keepRuleKey = options?.keepRuleKey !== false;
- if (className.endsWith('FunReplace')) {
- const fun = new FunReplace();
- fun.target = String(rule?.target ?? '');
- fun.replacement = String(rule?.replacement ?? '');
- return withSort(buildStoredFunPayload(fun));
- }
- if (className.endsWith('FunSpExtra')) {
- const fun = new FunSpExtra();
- fun.key = String(rule?.key ?? '');
- fun.index = Number(rule?.index ?? 1) || 1;
- return withSort(buildStoredFunPayload(fun));
- }
- if (className.endsWith('FunExtra')) {
- const fun = new FunExtra();
- fun.opened = rule?.opened !== false;
- fun.len = Number(rule?.len ?? 0) || 0;
- return withSort(buildStoredFunPayload(fun));
- }
- if (className.endsWith('FunDate')) {
- return withSort(buildStoredFunPayload(new FunDate()));
- }
- if (className.endsWith('FunTime')) {
- return withSort(buildStoredFunPayload(new FunTime()));
- }
- if (className.endsWith('FunDateTime')) {
- const payload = buildStoredFunPayload(new FunDateTime());
- if (keepRuleKey && normalizeRuleText(rule?.ruleKey) === 'timestampConversion') {
- return withSort({ ...payload, ruleKey: 'timestampConversion' });
- }
- return withSort(payload);
- }
- if (className.endsWith('FunRegular')) {
- const fun = new FunRegular();
- fun.regex = String(rule?.regex ?? '');
- return withSort(buildStoredFunPayload(fun));
- }
- if (className.endsWith('FunMultiplier')) {
- const fun = new FunMultiplier();
- fun.factor = Number(rule?.factor ?? 1) || 1;
- return withSort(buildStoredFunPayload(fun));
- }
- if (className.endsWith('FunAbs')) {
- const fun = new FunAbs();
- fun.opened = rule?.opened !== false;
- return withSort(buildStoredFunPayload(fun));
- }
- if (className.endsWith('FunRadix')) {
- const fun = new FunRadix();
- fun.radixType = Number(rule?.radixType ?? 1) || 1;
- fun.opened = toRuleBoolean(rule?.opened, false);
- return withSort(buildStoredFunPayload(fun));
- }
- return cloneDeepSafe(rule);
- };
- const sanitizeStoredColumnRulePayload = (rule?: TreeStoredColumnRule | null): TreeStoredColumnRule | null => {
- if (!rule) return null;
- const className = normalizeRuleText(rule?.['@class']);
- const mode = normalizeRuleText(rule?.mode);
- if (className.endsWith('RuleSplit') || mode === 'split') {
- const splitRule = new RuleSplit();
- const refIndex = Number(rule?.refIndex);
- const splitNameIndex = Number(rule?.splitNameIndex);
- const splitNameIndex2 = Number(rule?.splitNameIndex2);
- const len = Number(rule?.len);
- const mappingEnum = Number(rule?.mappingEnum);
- splitRule.refIndex = Number.isInteger(refIndex) && refIndex >= 0 ? refIndex : undefined;
- splitRule.mappingEnum = Number.isInteger(mappingEnum)
- ? mappingEnum
- : Number.isInteger(len) && len > 0
- ? MappingEnum.SP_LEN
- : MappingEnum.SP_CHAR;
- splitRule.splitNameIndex = Number.isInteger(splitNameIndex) && splitNameIndex >= 0 ? splitNameIndex : undefined;
- splitRule.splitNameIndex2 =
- Number.isInteger(splitNameIndex2) && splitNameIndex2 >= 0 ? splitNameIndex2 : undefined;
- splitRule.len = Number.isInteger(len) && len > 0 ? len : undefined;
- splitRule.splitChar = String(rule?.splitChar ?? '') || undefined;
- return buildStoredColumnRulePayload(splitRule);
- }
- if (className.endsWith('RuleMapping') || mode === 'ref' || mode === 'custom') {
- const mappingRule = new RuleMapping();
- mappingRule.refIndex = Array.isArray(rule?.refIndex)
- ? rule.refIndex.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
- : [];
- mappingRule.mappingEnum = Number(rule?.mappingEnum ?? MappingEnum.BLANK);
- mappingRule.content = String(rule?.content ?? '');
- return buildStoredColumnRulePayload(mappingRule);
- }
- return cloneDeepSafe(rule);
- };
- const sanitizeTreeTableRuleInfo = (
- rule: TreeTableRuleInfo,
- options?: { keepRuleKey?: boolean },
- ): TreeTableRuleInfo => {
- return {
- ...cloneDeepSafe(rule),
- funList: Array.isArray(rule.funList)
- ? rule.funList
- .map((item) => sanitizeStoredFunRulePayload(item, options))
- .filter((item): item is TreeStoredFunRule => Boolean(item))
- : rule.funList,
- columnRule: sanitizeStoredColumnRulePayload(rule.columnRule),
- };
- };
- const sanitizeTreeFileInfo = (item: TreeFileInfo, options?: { keepRuleKey?: boolean }): TreeFileInfo => ({
- ...cloneDeepSafe(item),
- tableRuleList: Array.isArray(item.tableRuleList)
- ? item.tableRuleList.map((rule) => sanitizeTreeTableRuleInfo(rule, options))
- : item.tableRuleList,
- children: Array.isArray(item.children)
- ? item.children.map((child) => sanitizeTreeFileInfo(child, options))
- : item.children,
- });
- const sanitizeFileInfoTree = (list: TreeFileInfo[], options?: { keepRuleKey?: boolean }) =>
- list.map((item) => sanitizeTreeFileInfo(item, options));
- const buildColumnRulePayload = (header: string): TreeStoredColumnRule | null => {
- const splitConfig = splitColumnConfig.value[header];
- if (splitConfig) {
- const splitNameIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName1);
- const splitNameIndex2 = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName2);
- const splitMode = normalizeSplitColumnMode(splitConfig.delimiterType);
- const splitValue = normalizeSplitColumnValue(splitConfig.delimiterType, splitConfig.delimiterValue);
- const splitLength = resolveSplitColumnLength(splitConfig.delimiterType, splitConfig.delimiterValue);
- const splitRule = new RuleSplit();
- splitRule.refIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.source);
- splitRule.mappingEnum = splitMode === 'length' ? MappingEnum.SP_LEN : MappingEnum.SP_CHAR;
- splitRule.splitNameIndex = splitNameIndex >= 0 ? splitNameIndex : undefined;
- splitRule.splitNameIndex2 = splitNameIndex2 >= 0 ? splitNameIndex2 : undefined;
- splitRule.len = splitMode === 'length' && splitLength > 0 ? splitLength : undefined;
- splitRule.splitChar = splitMode === 'special' && splitValue ? splitValue : undefined;
- return buildStoredColumnRulePayload(splitRule);
- }
- const addedConfig = addedColumnConfig.value[header];
- if (!addedConfig) return null;
- const mappingRule = new RuleMapping();
- mappingRule.refIndex = addedConfig.contentFields
- .map((field) => rightTableHeaders.value.findIndex((item) => item === field))
- .filter((index) => index >= 0);
- mappingRule.mappingEnum =
- addedConfig.mode === 'custom'
- ? addedConfig.customContent
- ? MappingEnum.NEW_DATA
- : MappingEnum.BLANK
- : mappingRule.refIndex.length
- ? MappingEnum.REF
- : MappingEnum.BLANK;
- mappingRule.content = addedConfig.mode === 'custom' ? addedConfig.customContent || '' : '';
- return buildStoredColumnRulePayload(mappingRule);
- };
- const restoreStoredColumnRules = (list?: TreeTableRuleInfo[] | null) => {
- const headerSnapshot = [...rightTableHeaders.value];
- const normalizedRules = normalizeTableRuleList(list);
- const splitRuleGroups = normalizedRules.reduce((acc, rule) => {
- const header = normalizeRuleText(rule.fileColName);
- const columnRule = rule.columnRule;
- const className = normalizeRuleText(columnRule?.['@class']);
- const mode = normalizeRuleText(columnRule?.mode);
- if (!header || (!className.endsWith('RuleSplit') && mode !== 'split')) {
- return acc;
- }
- const fileColIndex = Number(rule.fileColIndex);
- const identity = getSplitColumnRuleIdentity(columnRule);
- const prev = acc.get(identity) || [];
- prev.push({
- header,
- fileColIndex: Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : Number.MAX_SAFE_INTEGER,
- });
- acc.set(identity, prev);
- return acc;
- }, new Map<string, Array<{ header: string; fileColIndex: number }>>());
- normalizedRules.forEach((rule) => {
- const header = normalizeRuleText(rule.fileColName);
- if (!header) return;
- const columnRule = rule.columnRule;
- const fileColIndex = Number(rule.fileColIndex);
- const targetIndex = Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : undefined;
- if (columnRule) {
- insertHeaderAt(header, targetIndex);
- }
- const className = normalizeRuleText(columnRule?.['@class']);
- const mode = normalizeRuleText(columnRule?.mode);
- if (className.endsWith('RuleSplit') || mode === 'split') {
- const refIndex = Number(columnRule?.refIndex);
- const source =
- Number.isInteger(refIndex) && refIndex >= 0
- ? headerSnapshot[refIndex] || rightTableHeaders.value[refIndex] || ''
- : '';
- if (!source) return;
- const splitChar = String(columnRule?.splitChar ?? '');
- const splitLength = Number(columnRule?.len);
- const splitMappingEnum = Number(columnRule?.mappingEnum);
- const isLengthSplit =
- splitMappingEnum === MappingEnum.SP_LEN ||
- (!Number.isInteger(splitMappingEnum) && Number.isInteger(splitLength) && splitLength > 0);
- const splitNameIndex = Number(columnRule?.splitNameIndex);
- const splitNameIndex2 = Number(columnRule?.splitNameIndex2);
- const groupedTargetItems = (splitRuleGroups.get(getSplitColumnRuleIdentity(columnRule)) || [])
- .slice()
- .sort((a, b) => a.fileColIndex - b.fileColIndex);
- const groupedTargets = groupedTargetItems.map((item) => item.header).filter(Boolean);
- const uniqueTargets = Array.from(new Set(groupedTargets));
- const resolveGroupedTargetByIndex = (index: number) =>
- Number.isInteger(index) && index >= 0
- ? groupedTargetItems.find((item) => item.fileColIndex === index)?.header || ''
- : '';
- let targetName1 = resolveGroupedTargetByIndex(splitNameIndex);
- let targetName2 = resolveGroupedTargetByIndex(splitNameIndex2);
- if (!targetName1 && !targetName2) {
- targetName1 = uniqueTargets[0] || header;
- targetName2 = uniqueTargets.find((item) => item !== targetName1) || '';
- }
- if (targetName1 && targetName2 && targetName1 === targetName2) {
- if (Number.isInteger(fileColIndex) && fileColIndex === splitNameIndex2) {
- targetName1 = '';
- } else {
- targetName2 = '';
- }
- }
- splitColumnConfig.value[header] = {
- source,
- delimiterType: isLengthSplit ? 'length' : 'special',
- delimiterValue: isLengthSplit ? String(splitLength > 0 ? splitLength : '') : splitChar,
- targetName1,
- targetName2,
- };
- return;
- }
- if (className.endsWith('RuleMapping') || mode === 'ref' || mode === 'custom') {
- const refIndexList = Array.isArray(columnRule?.refIndex)
- ? columnRule.refIndex.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
- : [];
- const mappingEnum = Number(columnRule?.mappingEnum);
- const isRefMapping = mappingEnum === MappingEnum.REF || (!Number.isFinite(mappingEnum) && mode === 'ref');
- addedColumnConfig.value[header] = {
- mode: isRefMapping ? 'ref' : 'custom',
- contentFields: refIndexList
- .map((index) => headerSnapshot[index] || rightTableHeaders.value[index])
- .filter(Boolean),
- customContent: String(columnRule?.content ?? ''),
- };
- }
- });
- };
- const restoreStoredFunRules = (list?: TreeTableRuleInfo[] | null) => {
- selectedRules.value = [];
- selectRules.value = rightTableHeaders.value.map(() => []);
- const sharedRuleKeySet = new Set<string>();
- const mergeColumnRule = (
- columnRules: SelectedRuleItem[],
- nextRule: SelectedRuleItem,
- rawRule: TreeStoredFunRule,
- ) => {
- if (nextRule.key !== 'extractData') {
- columnRules.push(nextRule);
- return;
- }
- const lastRule = columnRules[columnRules.length - 1];
- if (!lastRule || lastRule.key !== 'extractData') {
- columnRules.push(nextRule);
- return;
- }
- const direction = normalizeRuleText(rawRule?.metaDirection) || (rawRule?.opened === false ? 'after' : 'before');
- if (direction === 'after') {
- lastRule.rules.nextModel = nextRule.rules.nextModel;
- } else {
- lastRule.rules.beforeModel = nextRule.rules.beforeModel;
- }
- };
- normalizeTableRuleList(list).forEach((tableRule) => {
- const header = normalizeRuleText(tableRule.fileColName);
- const columnIndex = header ? rightTableHeaders.value.findIndex((item) => item === header) : -1;
- const columnRules: SelectedRuleItem[] = [];
- (tableRule.funList || [])
- .slice()
- .sort((a, b) => Number(a?.sort ?? 0) - Number(b?.sort ?? 0))
- .forEach((rule) => {
- const nextRule = deserializeStoredRule(rule);
- if (!nextRule) return;
- if (nextRule.key === 'ignoreLine') {
- if (!sharedRuleKeySet.has(nextRule.key)) {
- selectedRules.value.push(nextRule);
- sharedRuleKeySet.add(nextRule.key);
- }
- return;
- }
- mergeColumnRule(columnRules, nextRule, rule);
- });
- if (columnIndex >= 0 && columnRules.length) {
- selectRules.value[columnIndex] = sanitizeSelectedRuleList(columnRules);
- }
- });
- selectedRules.value = sanitizeSelectedRuleList(selectedRules.value);
- };
- const restoreFileLevelRules = (item?: TreeFileInfo | null) => {
- const ignoreLines = Array.isArray(item?.ignoreLines)
- ? item.ignoreLines.map((line) => Number(line)).filter((line) => Number.isInteger(line) && line > 0)
- : [];
- if (ignoreLines.length) {
- const existingIgnoreRule = selectedRules.value.find((rule) => rule.key === 'ignoreLine');
- const nextIgnoreRules = {
- ...defaultRuleConfig(),
- ...(existingIgnoreRule?.rules || {}),
- omitLine: ignoreLines.join(','),
- };
- if (existingIgnoreRule) {
- existingIgnoreRule.rules = nextIgnoreRules;
- existingIgnoreRule.enabled = true;
- } else {
- selectedRules.value = [
- ...selectedRules.value,
- {
- name: ruleNameMap.ignoreLine,
- key: 'ignoreLine',
- active: selectedRules.value.length === 0,
- enabled: true,
- rules: nextIgnoreRules,
- },
- ];
- }
- }
- };
- const resolveTemplateFieldMeta = (field: MappingTargetField, index: number) => {
- const tableFields = getOrderedTemplateFields(
- templateDetail.value?.tableFields || activeTemplateItem.value?.tableFields,
- );
- return (
- tableFields.find(
- (item, itemIndex) =>
- normalizeRuleText(getTemplateFieldTargetKey(item, itemIndex)) === field.key ||
- normalizeRuleText(getTemplateFieldTargetLabel(item, itemIndex)) === field.label,
- ) || null
- );
- };
- const findTemplateFieldMetaByField = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
- const tableFields = getOrderedTemplateFields(
- templateDetail.value?.tableFields || activeTemplateItem.value?.tableFields,
- );
- return (
- tableFields.find(
- (item, index) =>
- normalizeRuleText(getTemplateFieldTargetKey(item, index)) === field.key ||
- normalizeRuleText(getTemplateFieldTargetLabel(item, index)) === normalizeRuleText(field.label),
- ) || null
- );
- };
- const buildTableRuleListFromState = (list?: TreeTableRuleInfo[] | null) => {
- const previousRules = normalizeTableRuleList(list);
- const currentTemplateTableId = normalizeTemplateId(
- templateDetail.value?.id ??
- activeTemplateItem.value?.id ??
- selectedTemplateId.value ??
- activeNodeInfo.value?.templateId,
- );
- const previousRuleMap = previousRules.reduce(
- (acc, item, index) => {
- const key = getTableRuleTargetKey(item, index);
- if (key) acc[key] = item;
- return acc;
- },
- {} as Record<string, TreeTableRuleInfo>,
- );
- const sharedFunRules = serializeStoredFunRules(selectedRules.value);
- const directionConfMap = recognitionDirectionFields.value.reduce(
- (acc, item) => {
- acc[item.key] = buildRecognitionDirectionConf(item.entries);
- return acc;
- },
- {} as Record<string, string>,
- );
- return fixedTargetFields.value.map((field, index) => {
- const previous = previousRuleMap[field.key];
- const requiredMarker = getTableRuleRequiredFlag(previous, field.required) || field.required;
- const mappedHeader = normalizeRuleText(effectiveFieldMapping.value[field.key]);
- const matchedByRecognitionRule = isTargetHandledByRecognitionRule(field.key);
- const fileColIndex = mappedHeader ? rightTableHeaders.value.findIndex((item) => item === mappedHeader) : -1;
- const columnFunRules =
- fileColIndex >= 0 ? serializeStoredFunRules(selectRules.value[fileColIndex] || [], sharedFunRules.length) : [];
- const templateField = resolveTemplateFieldMeta(field, index);
- const templateFieldRecord = (templateField || {}) as Record<string, any>;
- // 交易金额字段以「正负识别」开关为准(开关本身已从节点/模板的 zf 初始化);
- // 其余字段沿用模板字段 / 上一条规则的 zf 并兜底 0,保证每条规则都带整数 zf
- // ——后端 TableRuleDTO.zf 是 Integer,清洗器 `rule.getZf() == 1` 会自动拆箱,缺字段必 NPE。
- const zf = isTransAmountTargetField(field)
- ? recognitionAmountZfEnabled.value
- ? 1
- : 0
- : resolveStoredZfFlag(templateFieldRecord, previous);
- const requiredFlagValue = pickFirstDefinedRuleFlagValue(
- getRawRuleFlagValue(templateFieldRecord, 'required'),
- getRawRuleFlagValue(previous, 'required'),
- requiredMarker ? 1 : 0,
- );
- const matchedFlagValue =
- matchedByRecognitionRule || mappedHeader
- ? 1
- : pickFirstDefinedRuleFlagValue(
- getRawRuleFlagValue(templateFieldRecord, 'matched'),
- getRawRuleFlagValue(previous, 'matched'),
- requiredMarker ? 1 : 0,
- );
- const matchedRequiredFlagValue = pickFirstDefinedRuleFlagValue(getRawRuleFlagValue(previous, 'matchedRequired'));
- const directionConf =
- directionConfMap[field.key] ||
- normalizeRecognitionConfigText(previous?.directionConf) ||
- normalizeRecognitionConfigText(templateFieldRecord.directionConf ?? templateFieldRecord.direction_conf);
- const fileRuleList = [...(index === 0 ? sharedFunRules : []), ...columnFunRules];
- return {
- id: previous?.id ?? null,
- tableId: currentTemplateTableId || normalizeTemplateId(previous?.tableId) || null,
- fieldNameCn: field.label,
- fieldNameEn: field.key,
- fieldType: normalizeRuleText(templateField?.fieldType) || normalizeRuleText(previous?.fieldType) || 'string',
- required: requiredFlagValue,
- fieldLen: previous?.fieldLen ?? null,
- matched: matchedFlagValue,
- matchedRequired: matchedRequiredFlagValue,
- directionConf: directionConf || null,
- zf,
- fieldSort: index + 1,
- fileColIndex: fileColIndex >= 0 ? fileColIndex : null,
- fileColName: mappedHeader || null,
- funList: fileRuleList,
- columnRule: mappedHeader ? buildColumnRulePayload(mappedHeader) : null,
- } as TreeTableRuleInfo;
- });
- };
- const syncNodeStructure = (node?: TreeFileInfo | null) => {
- if (!isTreeSheetNode(node)) return;
- const templateId = normalizeTemplateId(
- templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value ?? node.templateId,
- );
- const mainId = getSelectedTemplateMainId(node.mainId);
- const tableName =
- normalizeRuleText(templateDetail.value?.tableNameCn) ||
- normalizeRuleText(activeTemplateItem.value?.tableNameCn) ||
- normalizeRuleText(node.tableName);
- const baseHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
- const existingHeaders = normalizeTemplateHeaders(node.header);
- const hasRealHeaderConfig =
- headerRowIndex.value !== null ||
- !!existingHeaders.length ||
- addedColumns.value.length > 0 ||
- Object.values(effectiveFieldMapping.value).some(Boolean) ||
- baseHeaders.length > 0;
- const shouldPersistRecognitionBasicInfo = hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value);
- const ignoreLines = Array.from(ignoredRowsSet.value).sort((a, b) => a - b);
- const groupColumns = buildEnabledGroupingColumnsPayload();
- node.templateId = templateId || node.templateId || null;
- node.mainId = mainId || null;
- node.tableName = tableName || node.tableName || null;
- if (headerRowIndex.value !== null) {
- node.lineNo = buildStoredLineNo(headerRowIndex.value);
- }
- if (hasRealHeaderConfig) {
- node.header = baseHeaders.length ? [...baseHeaders] : [];
- }
- node.ignoreLines = ignoreLines;
- node.groupColumns = groupColumns;
- node.tableRuleList = buildTableRuleListFromState(node.tableRuleList);
- node.childTemplateName = normalizeRuleText(childTemplateName.value) || null;
- node.hasNew = !!node.childTemplateName;
- if (shouldPersistRecognitionBasicInfo) {
- node.funcRegx = buildRecognitionBasicInfoFuncRegx();
- } else if (Object.prototype.hasOwnProperty.call(node, 'funcRegx')) {
- node.funcRegx = null;
- }
- persistFileListToSessionStorage();
- };
- const syncActiveNodeStructure = () => {
- if (structureSyncSuspended.value) return;
- syncNodeStructure(activeNodeInfo.value);
- };
- const visibleCleaningRules = computed(() => cleaningRulesList.value.filter((item) => item.show));
- const currentSelectedRules = computed(() => {
- if (isSharedRuleMode.value) {
- return sanitizeSelectedRuleList(selectedRules.value);
- }
- if (currentColumn.value === null) return [];
- return sanitizeSelectedRuleList(selectRules.value[currentColumn.value] || []);
- });
- const currentActiveSelectedRule = computed(() => currentSelectedRules.value.find((item) => item.active));
- const currentPickedRule = computed(() => visibleCleaningRules.value.find((item) => item.active));
- const currentPickedRuleKey = computed<CleaningRuleKey | undefined>(
- () => currentActiveSelectedRule.value?.key || currentPickedRule.value?.key,
- );
- const currentRuleExample = computed(() =>
- currentPickedRuleKey.value ? ruleExampleMap[currentPickedRuleKey.value] : undefined,
- );
- const currentActiveRuleDisplayName = computed(() =>
- currentPickedRuleKey.value ? ruleNameMap[currentPickedRuleKey.value] : '等待选择',
- );
- const getIgnoredRowsSetFromSelectedRules = (rules: SelectedRuleItem[] = []) => {
- const activeIgnoreRule = rules.find((rule) => rule.key === 'ignoreLine' && rule.enabled);
- const raw = String(activeIgnoreRule?.rules?.omitLine || '').trim();
- const nums = raw
- .split(',')
- .map((s) => Number(s.trim()))
- .filter((n) => Number.isInteger(n) && n > 0);
- return new Set(nums);
- };
- const ignoredRowsSet = computed(() => getIgnoredRowsSetFromSelectedRules(selectedRules.value));
- const currentColumnTitle = computed(() => {
- if (currentColumn.value === null) return '';
- return rightTableHeaders.value[currentColumn.value] || '';
- });
- const currentRuleEnabledLabel = computed(() => (currentActiveSelectedRule.value?.enabled ? '已启用' : '未启用'));
- const currentRuleEnabledClass = computed(() =>
- currentActiveSelectedRule.value?.enabled ? 'is-enabled' : 'is-disabled',
- );
- const currentRuleScopeType = computed(() => (isSharedRuleMode.value ? 'shared' : 'column'));
- const currentRuleScopeLabel = computed(() => (currentRuleScopeType.value === 'shared' ? '共享规则' : '列级规则'));
- const groupingHeaderOptions = computed(() =>
- rightTableHeaders.value.map((header) => ({
- label: header,
- value: header,
- })),
- );
- const isCurrentColumnAdded = computed(
- () => !!currentColumnTitle.value && addedColumns.value.includes(currentColumnTitle.value),
- );
- const activeNodeTableRuleMap = computed(() =>
- normalizeTableRuleList(findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw?.tableRuleList).reduce(
- (acc, item, index) => {
- const key = getTableRuleTargetKey(item, index);
- if (key) {
- acc[key] = item;
- }
- return acc;
- },
- {} as Record<string, TreeTableRuleInfo>,
- ),
- );
- const findActiveNodeTableRule = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
- const ruleByKey = activeNodeTableRuleMap.value[field.key];
- if (ruleByKey) {
- return ruleByKey;
- }
- const normalizedLabel = normalizeRuleText(field.label);
- if (!normalizedLabel) {
- return null;
- }
- return (
- normalizeTableRuleList(findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw?.tableRuleList).find(
- (item, index) => {
- const ruleLabel = normalizeRuleText(getTableRuleTargetLabel(item, index));
- return ruleLabel === normalizedLabel;
- },
- ) || null
- );
- };
- const isMappingTargetRequired = (field: Pick<MappingTargetField, 'key' | 'label' | 'required'>) => {
- const templateField = findTemplateFieldMetaByField(field);
- return getTableRuleRequiredFlag(
- findActiveNodeTableRule(field),
- getTemplateFieldRequiredFlag(templateField, field.required),
- );
- };
- const formatTypeLabelMap: Record<FormatValidationType, string> = {
- number: '数值',
- date: '日期',
- dateTime: '日期时间',
- time: '时间',
- loanFlag: '借贷标志',
- callType: '主被叫',
- phone: '号码',
- acctCardNo: '账号',
- idCardNumber: '身份证号',
- };
- const normalizeFormatValidationType = (fieldType?: string | null, fieldLabel = ''): FormatValidationType | '' => {
- const normalizedFieldType = normalizeRuleText(fieldType)
- .replace(/[\s-]+/g, '_')
- .toUpperCase();
- if (['TEXT', 'STRING', 'CHAR', 'VARCHAR', 'LONGTEXT', 'CLOB', '1'].includes(normalizedFieldType)) {
- return '';
- }
- if (['DATE_TIME', 'DATETIME', 'TIMESTAMP', '3'].includes(normalizedFieldType)) {
- return 'dateTime';
- }
- if (['DATE', 'DATE_ONLY', 'DATEONLY', '2'].includes(normalizedFieldType)) {
- return 'date';
- }
- if (['TIME', '6'].includes(normalizedFieldType)) {
- return 'time';
- }
- if (['PHONE', 'MOBILE', 'TEL', 'TELEPHONE'].includes(normalizedFieldType)) {
- return 'phone';
- }
- if (
- [
- 'ACCT_CARD_NO',
- 'ACCOUNT',
- 'ACCOUNT_NO',
- 'ACCOUNT_NUMBER',
- 'ACCT_NO',
- 'CARD_NO',
- 'BANK_CARD',
- 'BANK_CARD_NO',
- ].includes(normalizedFieldType)
- ) {
- return 'acctCardNo';
- }
- if (['DEBIT_CREDIT_FLAG', 'LOAN_FLAG'].includes(normalizedFieldType)) {
- return 'loanFlag';
- }
- if (['CALL_DIRECTION', 'CALL_TYPE'].includes(normalizedFieldType)) {
- return 'callType';
- }
- if (['ID_CARD_NUMBER', 'ID_CARD', 'IDENTITY_CARD', 'CERT_NO', 'CERTIFICATE_NO'].includes(normalizedFieldType)) {
- return 'idCardNumber';
- }
- if (
- [
- 'NUMBER',
- 'NUMERIC',
- 'DECIMAL',
- 'DOUBLE',
- 'FLOAT',
- 'INTEGER',
- 'INT',
- 'LONG',
- 'BIGINT',
- 'BIGDECIMAL',
- 'AMOUNT',
- 'MONEY',
- '4',
- '5',
- ].includes(normalizedFieldType)
- ) {
- return 'number';
- }
- if (normalizedFieldType) {
- return '';
- }
- const normalizedLabel = normalizeFieldMatchText(fieldLabel);
- if (!normalizedLabel) return '';
- if (/(号码状态|手机状态|手机号状态|电话状态)/.test(normalizedLabel)) {
- return '';
- }
- if (
- /(电话|联系电话|手机|手机号|手机号码|电话号码|本方号码|对方号码|主叫号码|被叫号码|号码)/.test(normalizedLabel) &&
- !/(类别|类型|归属地|地区|地址|编号|运单号|证件号|卡号|账号)/.test(normalizedLabel)
- ) {
- return 'phone';
- }
- if (
- /(金额|数值|数量|次数|时长|单价|价格|余额|积分|额度|里程|公里|重量|身高|年龄|税额|总价)/.test(normalizedLabel)
- ) {
- return 'number';
- }
- if (normalizedLabel.includes('日期时间')) {
- return 'dateTime';
- }
- if ((normalizedLabel.includes('日期') && normalizedLabel.includes('时间')) || normalizedLabel.includes('时刻')) {
- return 'dateTime';
- }
- if (normalizedLabel.includes('时间')) {
- return 'dateTime';
- }
- if (normalizedLabel.includes('日期') || normalizedLabel.includes('生日') || normalizedLabel.includes('出生')) {
- return 'date';
- }
- return '';
- };
- const isLeapYear = (year: number) => (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
- const getMonthDayCount = (year: number, month: number) =>
- [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] || 0;
- const isValidDateParts = (year: number, month: number, day: number) =>
- year >= 1000 && month >= 1 && month <= 12 && day >= 1 && day <= getMonthDayCount(year, month);
- const isValidTimeParts = (hour: number, minute: number, second = 0, fraction = '') =>
- hour >= 0 &&
- hour <= 23 &&
- minute >= 0 &&
- minute <= 59 &&
- second >= 0 &&
- second <= 59 &&
- (!fraction || /^\d{1,6}$/u.test(fraction));
- const normalizeFormatValue = (value: unknown) =>
- String(value ?? '')
- .replace(/\u00a0/g, ' ')
- .trim();
- const normalizeDateLikeText = (value: string) => {
- const normalized = normalizeFormatValue(value)
- .replace(/(Z|[+-]\d{2}:?\d{2})$/iu, '')
- .replace(/年/g, '-')
- .replace(/月/g, '-')
- .replace(/[日号]/g, ' ')
- .replace(/[Tt]/g, ' ')
- .replace(/\//g, '-')
- .replace(/^(\d{4})\.(\d{1,2})\.(\d{1,2})(.*)$/u, '$1-$2-$3$4')
- .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')
- .replace(/[点时]/g, ':')
- .replace(/分/g, ':')
- .replace(/秒/g, '')
- .replace(/\s+/g, ' ')
- .trim();
- return normalized.endsWith(':') ? normalized.slice(0, -1) : normalized;
- };
- const isValidDateOnlyValue = (value: string) => {
- const normalized = normalizeDateLikeText(value);
- const dashedMatch = normalized.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/u);
- if (dashedMatch) {
- return isValidDateParts(Number(dashedMatch[1]), Number(dashedMatch[2]), Number(dashedMatch[3]));
- }
- const compactMatch = normalized.match(/^(\d{4})(\d{2})(\d{2})$/u);
- if (compactMatch) {
- return isValidDateParts(Number(compactMatch[1]), Number(compactMatch[2]), Number(compactMatch[3]));
- }
- return false;
- };
- const isValidTimeOnlyValue = (value: string) => {
- const normalized = normalizeDateLikeText(value);
- const matched = normalized.match(/^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:[:.](\d{1,6}))?$/u);
- if (!matched) return false;
- return isValidTimeParts(Number(matched[1]), Number(matched[2]), Number(matched[3] || 0), matched[4] || '');
- };
- const isValidDateTimeValue = (value: string) => {
- const normalized = normalizeDateLikeText(value);
- if (isValidDateOnlyValue(normalized) || isValidTimeOnlyValue(normalized)) {
- return true;
- }
- const dashedMatch = normalized.match(
- /^(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:[:.](\d{1,6}))?$/u,
- );
- if (dashedMatch) {
- return (
- isValidDateParts(Number(dashedMatch[1]), Number(dashedMatch[2]), Number(dashedMatch[3])) &&
- isValidTimeParts(
- Number(dashedMatch[4]),
- Number(dashedMatch[5]),
- Number(dashedMatch[6] || 0),
- dashedMatch[7] || '',
- )
- );
- }
- const compactMatch = normalized.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{1,6})?$/u);
- if (compactMatch) {
- return (
- isValidDateParts(Number(compactMatch[1]), Number(compactMatch[2]), Number(compactMatch[3])) &&
- isValidTimeParts(
- Number(compactMatch[4]),
- Number(compactMatch[5]),
- Number(compactMatch[6]),
- compactMatch[7] || '',
- )
- );
- }
- return false;
- };
- const isValidNumberValue = (value: string) => {
- const normalized = normalizeFormatValue(value).replace(/[,\s,¥¥]/g, '');
- return /^[-+]?(?:\d+\.?\d*|\.\d+)$/u.test(normalized);
- };
- const isValidPhoneValue = (value: string) => {
- const normalized = normalizeFormatValue(value);
- if (!normalized) return true;
- if (/^(未知|空号|无|暂无)$/u.test(normalized)) {
- return true;
- }
- const compactValue = normalized.replace(/[\s\-()()+]/g, '');
- const digitOnlyValue = compactValue.replace(/\D/g, '');
- if (digitOnlyValue.length < 5) {
- return false;
- }
- if (/^[\d*#]+$/u.test(compactValue)) {
- return true;
- }
- return /^(\+?\d[\d\s\-()()]{4,}|\d{5,})$/u.test(normalized);
- };
- const isValidAcctCardNoValue = (value: string) => {
- const normalized = normalizeFormatValue(value);
- if (!normalized) return true;
- const compactValue = normalized.replace(/[\s\-()()]/g, '');
- if (!compactValue) return true;
- return /^[A-Za-z0-9*#]{6,32}$/u.test(compactValue);
- };
- const getIdCardCheckCode = (body: string) => {
- const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
- const codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
- const total = body.split('').reduce((sum, char, index) => sum + Number(char) * weights[index], 0);
- return codes[total % 11];
- };
- const isValidIdCardNumberValue = (value: string) => {
- const normalized = normalizeFormatValue(value);
- if (!normalized) return true;
- const compactValue = normalized.replace(/\s/g, '').toUpperCase();
- if (/^\d{15}$/u.test(compactValue)) {
- const year = Number(`19${compactValue.slice(6, 8)}`);
- const month = Number(compactValue.slice(8, 10));
- const day = Number(compactValue.slice(10, 12));
- return isValidDateParts(year, month, day);
- }
- if (!/^\d{17}[\dX]$/u.test(compactValue)) {
- return false;
- }
- const year = Number(compactValue.slice(6, 10));
- const month = Number(compactValue.slice(10, 12));
- const day = Number(compactValue.slice(12, 14));
- if (!isValidDateParts(year, month, day)) {
- return false;
- }
- return getIdCardCheckCode(compactValue.slice(0, 17)) === compactValue[17];
- };
- const loanFlagCoreValueMap: Record<string, string> = {
- 收入: '收入',
- 支出: '支出',
- 借方: '收入',
- 贷方: '支出',
- 借: '收入',
- 贷: '支出',
- };
- const normalizeLoanFlagCoreValue = (value: string) => {
- const normalized = normalizeFormatValue(value).replace(/[\s_\-()()/]/g, '');
- if (!normalized) return '';
- return loanFlagCoreValueMap[normalized] || '';
- };
- const callTypeCoreValueMap: Record<string, string> = {
- 主叫: '主叫',
- 被叫: '被叫',
- 发送: '发送',
- 接收: '接收',
- 呼入: '呼入',
- 呼出: '呼出',
- 上行: '上行',
- 下行: '下行',
- 来电: '呼入',
- 去电: '呼出',
- CALLIN: '被叫',
- CALLOUT: '主叫',
- SMSIN: '接收',
- SMSOUT: '发送',
- };
- const normalizeCallTypeCoreValue = (value: string) => {
- const normalized = normalizeFormatValue(value)
- .toUpperCase()
- .replace(/[\s_\-()()/]/g, '');
- if (!normalized) return '';
- if (callTypeCoreValueMap[normalized]) {
- return callTypeCoreValueMap[normalized];
- }
- const simplified = normalized.replace(/短信|彩信|通话|话单|记录|类型|类别|方向/g, '');
- return callTypeCoreValueMap[simplified] || '';
- };
- const isValidLoanFlagValue = (value: string, targetKey = '') => {
- const normalized = normalizeFormatValue(value);
- if (!normalized) return true;
- const matchedByConfig = matchRecognitionDirectionConfigValue(normalized, '借贷标志', targetKey);
- if (matchedByConfig !== null) return matchedByConfig;
- return !!normalizeLoanFlagCoreValue(normalized);
- };
- const isValidCallTypeValue = (value: string, targetKey = '') => {
- const normalized = normalizeFormatValue(value);
- if (!normalized) return true;
- const matchedByConfig = matchRecognitionDirectionConfigValue(normalized, '通信类别', targetKey);
- if (matchedByConfig !== null) return matchedByConfig;
- return !!normalizeCallTypeCoreValue(normalized);
- };
- const isValueValidByFormatType = (value: string, formatType: FormatValidationType, targetKey = '') => {
- switch (formatType) {
- case 'number':
- return isValidNumberValue(value);
- case 'date':
- return isValidDateOnlyValue(value);
- case 'dateTime':
- return isValidDateTimeValue(value);
- case 'time':
- return isValidTimeOnlyValue(value);
- case 'loanFlag':
- return isValidLoanFlagValue(value, targetKey);
- case 'callType':
- return isValidCallTypeValue(value, targetKey);
- case 'phone':
- return isValidPhoneValue(value);
- case 'acctCardNo':
- return isValidAcctCardNoValue(value);
- case 'idCardNumber':
- return isValidIdCardNumberValue(value);
- default:
- return true;
- }
- };
- const targetFormatMetaMap = computed(() =>
- fixedTargetFields.value.reduce(
- (acc, field, index) => {
- const templateFieldRecord = (resolveTemplateFieldMeta(field, index) || {}) as Record<string, any>;
- const rawFieldType = normalizeRuleText(
- templateFieldRecord.fieldType ??
- templateFieldRecord.field_type ??
- activeNodeTableRuleMap.value[field.key]?.fieldType,
- );
- acc[field.key] = {
- key: field.key,
- label: field.label,
- formatType: normalizeFormatValidationType(rawFieldType, field.label),
- };
- return acc;
- },
- {} as Record<string, { key: string; label: string; formatType: FormatValidationType | '' }>,
- ),
- );
- const matchedTargetFieldsByHeader = computed(() => {
- return Object.entries(effectiveFieldMapping.value).reduce(
- (acc, [targetKey, header]) => {
- const mappedHeader = normalizeRuleText(header);
- if (!mappedHeader || !acc[mappedHeader]) return acc;
- const targetMeta = targetFormatMetaMap.value[targetKey];
- if (targetMeta) {
- acc[mappedHeader].push(targetMeta);
- }
- return acc;
- },
- rightTableHeaders.value.reduce(
- (acc, header) => {
- acc[header] = [];
- return acc;
- },
- {} as Record<string, Array<{ key: string; label: string; formatType: FormatValidationType | '' }>>,
- ),
- );
- });
- const previewNormalizationTargetMetaByHeader = computed(() =>
- rightTableHeaders.value.reduce(
- (acc, header) => {
- const matchedTarget = (matchedTargetFieldsByHeader.value[header] || []).find((item) =>
- ['loanFlag', 'callType'].includes(item.formatType),
- );
- if (matchedTarget?.formatType) {
- acc[header] = matchedTarget as {
- key: string;
- label: string;
- formatType: 'loanFlag' | 'callType';
- };
- }
- return acc;
- },
- {} as Record<string, { key: string; label: string; formatType: 'loanFlag' | 'callType' }>,
- ),
- );
- const resolveRecognitionDirectionMatchedGroup = (
- value: string,
- tab: RecognitionRuleTabKey,
- targetKey = '',
- entries?: RecognitionDirectionItem[],
- ) => {
- const normalizedValue = normalizeRecognitionConfigText(value);
- if (!normalizedValue) return '';
- const fieldEntries =
- entries ||
- recognitionDirectionFields.value.find((item) => item.key === targetKey && item.tab === tab)?.entries ||
- [];
- const matchedEntry = fieldEntries.find((entry) => {
- const comparableTexts = [entry.label, ...splitRecognitionDirectionKeywords(entry.value)];
- return comparableTexts.some((keyword) => {
- const normalizedKeyword = normalizeRecognitionConfigText(keyword);
- return !!normalizedKeyword && normalizedKeyword === normalizedValue;
- });
- });
- if (matchedEntry) {
- return (
- normalizeRecognitionDirectionGroup(matchedEntry.label, tab) ||
- normalizeRecognitionConfigText(matchedEntry.label)
- );
- }
- return '';
- };
- const normalizePreviewFormatValue = (
- value: string,
- targetMeta?: { key: string; label: string; formatType: 'loanFlag' | 'callType' },
- ) => {
- const normalizedValue = String(value ?? '');
- if (!targetMeta || !normalizedValue) return normalizedValue;
- if (targetMeta.formatType === 'loanFlag') {
- return resolveRecognitionDirectionMatchedGroup(normalizedValue, '借贷标志', targetMeta.key) || normalizedValue;
- }
- return resolveRecognitionDirectionMatchedGroup(normalizedValue, '通信类别', targetMeta.key) || normalizedValue;
- };
- const normalizePreviewRowsByTargetFormat = (rows: Record<string, string>[]) =>
- rows.map((row, rowIndex) => {
- if (headerRowIndex.value === null || rowIndex <= headerRowIndex.value || ignoredRowsSet.value.has(rowIndex + 1)) {
- return row;
- }
- let nextRow: Record<string, string> | null = null;
- Object.entries(previewNormalizationTargetMetaByHeader.value).forEach(([header, targetMeta]) => {
- const normalizedCellValue = normalizePreviewFormatValue(String(row?.[header] ?? ''), targetMeta);
- if (normalizedCellValue === String(row?.[header] ?? '')) return;
- if (!nextRow) {
- nextRow = { ...row };
- }
- nextRow[header] = normalizedCellValue;
- });
- return nextRow || row;
- });
- const formatValidationRows = computed(() => {
- const currentHeaderRowIndex = headerRowIndex.value;
- if (currentHeaderRowIndex === null) {
- return [] as Record<string, string>[];
- }
- return tablePreviewRows.value.filter((_row, rowIndex) => {
- const lineNumber = rowIndex + 1;
- if (ignoredRowsSet.value.has(lineNumber)) return false;
- return rowIndex > currentHeaderRowIndex;
- });
- });
- const buildFormatValidationTitle = (invalidTargets: HeaderFormatValidationState['invalidTargets']) =>
- invalidTargets
- .map(({ label, formatType, invalidValue }) => {
- const shortValue = invalidValue.length > 18 ? `${invalidValue.slice(0, 18)}...` : invalidValue;
- return `${label}需符合${formatTypeLabelMap[formatType]}格式,当前检测到“${shortValue}”`;
- })
- .join(';');
- const shouldSkipFormatValidationByTarget = (targetMeta: {
- key: string;
- label: string;
- formatType: FormatValidationType;
- }) => {
- const normalizedLabel = normalizeFieldMatchText(targetMeta.label);
- return normalizedLabel === '号码归属省' || normalizedLabel === '号码归属市';
- };
- const getHeaderFormatValidationCandidates = (header: string) =>
- Array.from(
- (matchedTargetFieldsByHeader.value[header] || [])
- .reduce((candidateMap, targetMeta) => {
- if (!targetMeta.formatType) return candidateMap;
- if (
- shouldSkipFormatValidationByTarget(
- targetMeta as {
- key: string;
- label: string;
- formatType: FormatValidationType;
- },
- )
- ) {
- return candidateMap;
- }
- candidateMap.set(`${targetMeta.key}:${targetMeta.formatType}`, {
- key: targetMeta.key,
- label: targetMeta.label,
- formatType: targetMeta.formatType,
- });
- return candidateMap;
- }, new Map<string, { key: string; label: string; formatType: FormatValidationType }>())
- .values(),
- );
- const headerFormatValidationMap = computed(() =>
- rightTableHeaders.value.reduce(
- (acc, header) => {
- const invalidTargets = getHeaderFormatValidationCandidates(header).reduce(
- (list, targetMeta) => {
- const formatType = targetMeta.formatType;
- const invalidValue =
- formatValidationRows.value.find((row) => {
- const cellValue = normalizeFormatValue(row[header]);
- return !!cellValue && !isValueValidByFormatType(cellValue, formatType, targetMeta.key);
- })?.[header] || '';
- const normalizedInvalidValue = normalizeFormatValue(invalidValue);
- if (!normalizedInvalidValue) return list;
- list.push({
- key: targetMeta.key,
- label: targetMeta.label,
- formatType,
- invalidValue: normalizedInvalidValue,
- });
- return list;
- },
- [] as HeaderFormatValidationState['invalidTargets'],
- );
- acc[header] = {
- invalidTargets,
- title: buildFormatValidationTitle(invalidTargets),
- };
- return acc;
- },
- {} as Record<string, HeaderFormatValidationState>,
- ),
- );
- const hasHeaderFormatError = (header: string) => !!headerFormatValidationMap.value[header]?.invalidTargets.length;
- const getHeaderFormatErrorTitle = (header: string) => headerFormatValidationMap.value[header]?.title || '';
- const getTargetFormatErrorState = (targetKey: string) => {
- const mappedHeader = effectiveFieldMapping.value[targetKey];
- if (!mappedHeader) return null;
- return headerFormatValidationMap.value[mappedHeader]?.invalidTargets.find((item) => item.key === targetKey) || null;
- };
- const hasTargetFormatError = (targetKey: string) => !!getTargetFormatErrorState(targetKey);
- const getTargetFormatErrorTitle = (targetKey: string) => {
- const matchedState = getTargetFormatErrorState(targetKey);
- if (!matchedState) return '';
- return buildFormatValidationTitle([matchedState]);
- };
- const getMatchedTargetFieldText = (header: string) =>
- matchedTargetFieldsByHeader.value[header]?.map((item) => item.label).join('、') || '';
- const hasMatchedTargetField = (header: string) => !!matchedTargetFieldsByHeader.value[header]?.length;
- const getMatchedHeaderStatusClass = (header: string) => (hasMatchedTargetField(header) ? 'is-success' : 'is-fail');
- const getMatchedHeaderStatusText = (header: string) => (hasMatchedTargetField(header) ? '✓' : '✕');
- const getMatchedHeaderStatusTitle = (header: string) => (hasMatchedTargetField(header) ? '已匹配' : '未匹配');
- const normalizeHeaderLabel = (value: string | number | null | undefined, index: number) => {
- const normalized = String(value ?? '')
- .trim()
- .replace(/\s+/g, ' ');
- return normalized || `未命名列${index + 1}`;
- };
- const buildHeaderLabelsByRow = (
- previousHeaders: string[],
- row: Record<string, string>,
- fixedHeaders: string[] = [],
- ) => {
- const fixedHeaderSet = new Set(fixedHeaders);
- const usedLabels = new Set(fixedHeaders);
- return previousHeaders.map((header, index) => {
- if (fixedHeaderSet.has(header)) {
- return header;
- }
- const baseName = normalizeHeaderLabel(row[header], index);
- if (!usedLabels.has(baseName)) {
- usedLabels.add(baseName);
- return baseName;
- }
- let suffix = 2;
- let candidate = `${baseName}_${suffix}`;
- while (usedLabels.has(candidate)) {
- suffix += 1;
- candidate = `${baseName}_${suffix}`;
- }
- usedLabels.add(candidate);
- return candidate;
- });
- };
- const renameRecordKeys = <T,>(record: Record<string, T>, keyMap: Record<string, string>) => {
- return Object.entries(record).reduce(
- (acc, [key, value]) => {
- acc[keyMap[key] || key] = value;
- return acc;
- },
- {} as Record<string, T>,
- );
- };
- const buildStoredFunPayload = <T extends object>(
- payload: T,
- extras?: Partial<TreeStoredFunRule>,
- ): TreeStoredFunRule => ({
- ...(cloneDeepSafe({ ...payload }) as TreeStoredFunRule),
- ...(extras || {}),
- });
- const buildStoredColumnRulePayload = <T extends object>(payload: T): TreeStoredColumnRule =>
- cloneDeepSafe({ ...payload }) as TreeStoredColumnRule;
- const splitRegexTokens = (value: string) => {
- const tokens: string[] = [];
- let current = '';
- let escaped = false;
- for (const char of value) {
- if (!escaped && char === '|') {
- tokens.push(current);
- current = '';
- continue;
- }
- current += char;
- escaped = !escaped && char === '\\';
- if (char !== '\\') {
- escaped = false;
- }
- }
- if (current) {
- tokens.push(current);
- }
- return tokens;
- };
- const parseRemoveSpecialRegex = (regex: string) => {
- const normalized = String(regex || '').trim();
- if (!normalized) {
- return { specialList: [] as string[], specialStr: '' };
- }
- const body =
- normalized.startsWith('(?:') && normalized.endsWith(')')
- ? normalized.slice(3, normalized.length - 1)
- : normalized;
- const tokens = splitRegexTokens(body);
- const specialList: string[] = [];
- const customTokens: string[] = [];
- tokens.forEach((token) => {
- if (!token) return;
- if (token === '\\s') {
- specialList.push('移除空格字符');
- return;
- }
- if (token === '\\r\\n') {
- specialList.push('移除换行字符(tab)');
- return;
- }
- customTokens.push(token.replace(/\\(.)/g, '$1'));
- });
- if (customTokens.length) {
- specialList.push('要移除的特殊字符');
- }
- return {
- specialList,
- specialStr: customTokens.join('|'),
- };
- };
- const getSplitColumnRuleIdentity = (rule?: TreeStoredColumnRule | null) => {
- const refIndex = Number(rule?.refIndex);
- const splitNameIndex = Number(rule?.splitNameIndex);
- const splitNameIndex2 = Number(rule?.splitNameIndex2);
- const len = Number(rule?.len);
- const splitChar = String(rule?.splitChar ?? '');
- return JSON.stringify({
- refIndex: Number.isInteger(refIndex) ? refIndex : null,
- mappingEnum: Number(rule?.mappingEnum ?? MappingEnum.SP_CHAR),
- splitNameIndex: Number.isInteger(splitNameIndex) ? splitNameIndex : null,
- splitNameIndex2: Number.isInteger(splitNameIndex2) ? splitNameIndex2 : null,
- len: Number.isInteger(len) && len > 0 ? len : null,
- splitChar,
- });
- };
- const remapGroupingColumnRules = (
- list: Array<Partial<GroupingColumnDraft> | Record<string, any>> | undefined,
- keyMap: Record<string, string>,
- removedHeaders: Set<string> = new Set(),
- ) =>
- normalizeGroupingColumnsDraft(list).map((item) => {
- const nextHeader = removedHeaders.has(item.header) ? '' : keyMap[item.header] || item.header;
- return createGroupingColumnDraft({
- ...item,
- header: nextHeader,
- title: nextHeader || '',
- });
- });
- const remapSelectRuleHeaderRefs = (
- groups: SelectedRuleItem[][],
- keyMap: Record<string, string>,
- removedHeaders: Set<string> = new Set(),
- ) =>
- groups.map((rules) =>
- rules.map((rule) => {
- if (rule.key !== 'groupingColumns') {
- return rule;
- }
- return {
- ...rule,
- rules: {
- ...rule.rules,
- groupColumns: remapGroupingColumnRules(rule.rules?.groupColumns, keyMap, removedHeaders),
- },
- };
- }),
- );
- const syncHeaderRowInput = () => {
- headerRowInput.value = headerRowIndex.value === null ? '' : String(headerRowIndex.value + 1);
- };
- const applyHeaderRow = (rowIndex: number, options: { silent?: boolean; autoMatch?: boolean } = {}) => {
- const { silent = false, autoMatch = true } = options;
- if (rowIndex < 0 || rowIndex >= sourceTableRows.value.length) {
- syncHeaderRowInput();
- if (!silent) {
- createMessage.warning('请输入正确的表头行号');
- }
- return;
- }
- const previousHeaders = [...rightTableHeaders.value];
- const headerSourceRow = sourceTableRows.value[rowIndex] || ({} as Record<string, string>);
- const nextHeaders = buildHeaderLabelsByRow(previousHeaders, headerSourceRow, addedColumns.value);
- if (
- headerRowIndex.value === rowIndex &&
- nextHeaders.length === previousHeaders.length &&
- nextHeaders.every((header, index) => header === previousHeaders[index])
- ) {
- selectedRowIndex.value = rowIndex;
- syncHeaderRowInput();
- return;
- }
- const headerKeyMap = previousHeaders.reduce(
- (acc, header, index) => {
- acc[header] = nextHeaders[index];
- return acc;
- },
- {} as Record<string, string>,
- );
- const fixedHeaderSet = new Set(addedColumns.value);
- sourceTableRows.value = sourceTableRows.value.map((row, index) => {
- const nextRow: Record<string, string> = {};
- previousHeaders.forEach((header, colIndex) => {
- const nextHeader = nextHeaders[colIndex];
- if (fixedHeaderSet.has(header)) {
- nextRow[nextHeader] = String(row[header] ?? '');
- return;
- }
- nextRow[nextHeader] = index === rowIndex ? nextHeader : String(row[header] ?? '');
- });
- return nextRow;
- });
- rightTableHeaders.value = nextHeaders;
- generatedColumnHeaderRowMap.value = addedColumns.value.reduce(
- (acc, item) => {
- const nextKey = headerKeyMap[item] || item;
- acc[nextKey] = getGeneratedColumnHeaderRowIndex(item);
- return acc;
- },
- {} as Record<string, number | null>,
- );
- addedColumns.value = addedColumns.value.map((item) => headerKeyMap[item] || item);
- selectedAddedColumn.value = selectedAddedColumn.value ? headerKeyMap[selectedAddedColumn.value] || '' : '';
- fieldMapping.value = Object.entries(fieldMapping.value).reduce(
- (acc, [key, value]) => {
- acc[key] = headerKeyMap[value] || value;
- return acc;
- },
- {} as Record<string, string>,
- );
- addedColumnConfig.value = Object.entries(renameRecordKeys(addedColumnConfig.value, headerKeyMap)).reduce(
- (acc, [key, value]) => {
- acc[key] = {
- ...value,
- contentFields: value.contentFields.map((field) => headerKeyMap[field] || field),
- };
- return acc;
- },
- {} as Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>,
- );
- splitColumnConfig.value = Object.entries(renameRecordKeys(splitColumnConfig.value, headerKeyMap)).reduce(
- (acc, [key, value]) => {
- acc[key] = {
- ...value,
- source: headerKeyMap[value.source] || value.source,
- targetName1: headerKeyMap[value.targetName1] || value.targetName1,
- targetName2: headerKeyMap[value.targetName2] || value.targetName2,
- };
- return acc;
- },
- {} as Record<
- string,
- { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
- >,
- );
- selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, headerKeyMap);
- groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, headerKeyMap);
- if (splitColumnForm.value.source) {
- splitColumnForm.value.source = headerKeyMap[splitColumnForm.value.source] || splitColumnForm.value.source;
- }
- headerRowIndex.value = rowIndex;
- selectedRowIndex.value = rowIndex;
- syncHeaderRowInput();
- rebuildGeneratedColumnsInSourceRows();
- runRulePipeline();
- if (autoMatch) {
- autoMatchFields(false);
- }
- if (!silent) {
- createMessage.success(`已将第${rowIndex + 1}行设为表头行`);
- }
- };
- const applyHeaderRowInput = () => {
- const rawValue = headerRowInput.value.trim();
- if (!rawValue) {
- syncHeaderRowInput();
- return;
- }
- const rowNumber = Number(rawValue);
- if (!Number.isInteger(rowNumber)) {
- syncHeaderRowInput();
- createMessage.warning('请输入正确的表头行号');
- return;
- }
- applyHeaderRow(rowNumber - 1);
- };
- const restorePreviewFromNode = async (item?: TreeFileInfo | null) => {
- const previewDataset = await loadPreviewDatasetFromFile(item);
- const hasPresetHeaders = previewDataset ? applyPreviewDataset(previewDataset) : applyPresetHeadersFromNode(item);
- const headerRow = normalizeStoredLineNoToRowIndex(item?.lineNo);
- if (headerRow !== null && !hasPresetHeaders) {
- applyHeaderRow(headerRow, { silent: true, autoMatch: false });
- return;
- }
- headerRowIndex.value = headerRow;
- selectedRowIndex.value = headerRow;
- syncHeaderRowInput();
- };
- const ensureCurrentHeaderRowConfigured = (warningText = '请先填写表头所在行,再进行模板匹配') => {
- if (headerRowIndex.value !== null) return true;
- const rawValue = headerRowInput.value.trim();
- if (!rawValue) {
- createMessage.warning(warningText);
- return false;
- }
- const rowNumber = Number(rawValue);
- if (!Number.isInteger(rowNumber) || rowNumber <= 0 || rowNumber > sourceTableRows.value.length) {
- createMessage.warning('请输入正确的表头行号');
- return false;
- }
- applyHeaderRow(rowNumber - 1, { silent: true, autoMatch: false });
- return headerRowIndex.value !== null;
- };
- const hasNodeTemplateMatched = (item?: TreeFileInfo | null) => {
- if (!isTreeSheetNode(item)) return false;
- return (
- normalizeTemplateId(item?.templateId) > 0 ||
- normalizeTableRuleList(item?.tableRuleList).length > 0 ||
- !!normalizeRuleText(item?.tableName)
- );
- };
- const collectMatchedSheetsWithoutHeaderRow = (items?: TreeFileInfo[] | null) => {
- const missingSheets: string[] = [];
- const walk = (nodes?: TreeFileInfo[] | null) => {
- if (!Array.isArray(nodes) || !nodes.length) return;
- nodes.forEach((node) => {
- if (hasNodeTemplateMatched(node) && normalizeStoredLineNoToRowIndex(node?.lineNo) === null) {
- missingSheets.push(getTreeSheetName(node));
- }
- if (Array.isArray(node.children) && node.children.length) {
- walk(node.children);
- }
- });
- };
- walk(items);
- return missingSheets;
- };
- const mockIndexColumnWidth = 72;
- const mockDataColumnWidth = 168;
- const mockGridTemplate = computed(() =>
- [`${mockIndexColumnWidth}px`, ...rightTableHeaders.value.map(() => `${mockDataColumnWidth}px`)].join(' '),
- );
- const getColumnRuleBadgeState = (colIndex: number) => {
- if (colIndex < 0) return false;
- const rules = sanitizeSelectedRuleList(selectRules.value[colIndex] || []);
- const hasEnabledRules = rules.some((rule) => rule && rule.key && rule.enabled);
- if (hasEnabledRules) return 'active';
- return rules.some((rule) => rule && rule.key) ? 'draft' : '';
- };
- const getColumnRuleBadgeTitle = (colIndex: number) => {
- const state = getColumnRuleBadgeState(colIndex);
- if (state === 'active') return '该列已启用清洗流程';
- if (state === 'draft') return '该列已添加清洗规则,待启用';
- return '';
- };
- const addColumnDialogOpen = ref(false);
- const splitColumnDialogOpen = ref(false);
- const deleteColumnDialogOpen = ref(false);
- const addColumnMode = ref<'add' | 'edit'>('add');
- const addColumnForm = ref<{
- name: string;
- mode: 'ref' | 'custom';
- contentFields: string[];
- customContent: string;
- }>({
- name: '',
- mode: 'ref',
- contentFields: [],
- customContent: '',
- });
- const addedColumnConfig = ref<
- Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>
- >({});
- const splitColumnConfig = ref<
- Record<
- string,
- { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
- >
- >({});
- const selectedSourceField = ref('');
- const selectedContentIndex = ref(-1);
- const draggingContentIndex = ref<number | null>(null);
- const dragOverContentIndex = ref<number | null>(null);
- const availableSourceHeaders = computed(() =>
- rightTableHeaders.value.filter((item) => item && !addedColumns.value.includes(item)),
- );
- const sourceFieldCandidates = computed(() => availableSourceHeaders.value);
- const normalizeColumnNameForCompare = (value: unknown) =>
- String(value ?? '')
- .trim()
- .toLocaleLowerCase();
- const hasDuplicateHeaderName = (name: string, excludedRawNames: string[] = []) => {
- const normalizedName = normalizeColumnNameForCompare(name);
- if (!normalizedName) return false;
- return rightTableHeaders.value.some(
- (header) => !excludedRawNames.includes(header) && normalizeColumnNameForCompare(header) === normalizedName,
- );
- };
- const normalizeSplitColumnMode = (value?: unknown) => {
- const rawMode = normalizeRuleText(value);
- if (rawMode === 'length' || rawMode === 'special') {
- return rawMode as 'length' | 'special';
- }
- if (rawMode === 'comma' || rawMode === 'space' || rawMode === 'custom') {
- return 'special' as const;
- }
- return 'length' as const;
- };
- const normalizeSplitColumnValue = (type?: unknown, value?: unknown) => {
- const rawType = normalizeRuleText(type);
- if (rawType === 'comma') return ',';
- if (rawType === 'space') return ' ';
- return normalizeRuleText(value);
- };
- const resolveSplitColumnLength = (type?: unknown, value?: unknown) => {
- if (normalizeSplitColumnMode(type) !== 'length') return 0;
- const length = Number(normalizeSplitColumnValue(type, value));
- return Number.isInteger(length) && length > 0 ? length : 0;
- };
- const splitSourceValueByConfig = (sourceValue: string, type?: unknown, value?: unknown) => {
- const splitMode = normalizeSplitColumnMode(type);
- const normalizedValue = normalizeSplitColumnValue(type, value);
- if (!sourceValue) {
- return { first: '', second: '' };
- }
- if (splitMode === 'length') {
- const splitLength = resolveSplitColumnLength(type, value);
- if (!splitLength) {
- return { first: sourceValue, second: '' };
- }
- return {
- first: sourceValue.slice(0, splitLength),
- second: sourceValue.slice(splitLength),
- };
- }
- if (!normalizedValue) {
- return { first: sourceValue, second: '' };
- }
- const splitIndex = sourceValue.indexOf(normalizedValue);
- if (splitIndex < 0) {
- return { first: sourceValue, second: '' };
- }
- return {
- first: sourceValue.slice(0, splitIndex),
- second: sourceValue.slice(splitIndex + normalizedValue.length),
- };
- };
- const splitColumnForm = ref({
- source: '',
- targetName1: '',
- targetName2: '',
- delimiterType: 'length',
- delimiterValue: '',
- });
- const delimiterOptions = [
- { label: '按字符长度', value: 'length' },
- { label: '按特殊标识', value: 'special' },
- ];
- const columnTypeOptions = [
- { label: '字符串', value: 'string' },
- { label: '数字', value: 'number' },
- { label: '日期时间', value: 'datetime' },
- ];
- const splitSourceOptions = computed(() =>
- availableSourceHeaders.value.map((item) => ({
- label: item,
- value: item,
- })),
- );
- const originalFieldOptions = computed(() =>
- rightTableHeaders.value.map((item) => ({
- label: item,
- value: item,
- })),
- );
- const fieldMapping = ref<Record<string, string>>({});
- const mappingPopoverOpenKey = ref<string>('');
- const originFilterKeyword = ref('');
- const hoveredOriginOptionTargetKey = ref('');
- const hoveredOriginOptionValue = ref('');
- const originHoverPreviewSnapshot = ref<PreviewColumnSnapshot | null>(null);
- const recognitionAmountZfEnabled = ref(false);
- const resolveTargetKeyByTableRule = (rule: TreeTableRuleInfo) => {
- const fieldNameEn = normalizeRuleText(rule.fieldNameEn);
- if (fieldNameEn) {
- const matchedByKey = fixedTargetFields.value.find((item) => item.key === fieldNameEn);
- if (matchedByKey) return matchedByKey.key;
- }
- const fieldNameCn = normalizeRuleText(rule.fieldNameCn);
- if (fieldNameCn) {
- const matchedByLabel =
- fixedTargetFields.value.find((item) => item.label === fieldNameCn) ||
- fixedTargetFields.value.find((item) => item.label.includes(fieldNameCn) || fieldNameCn.includes(item.label));
- if (matchedByLabel) return matchedByLabel.key;
- }
- return '';
- };
- const applyTableRuleMapping = (list?: TreeTableRuleInfo[] | null) => {
- const tableRules = normalizeTableRuleList(list);
- if (!tableRules.length || !fixedTargetFields.value.length) return false;
- const headerSet = new Set(rightTableHeaders.value);
- const nextMapping = tableRules.reduce(
- (acc, item) => {
- const targetKey = resolveTargetKeyByTableRule(item);
- const sourceField = normalizeRuleText(item.fileColName);
- if (!targetKey || !sourceField || !headerSet.has(sourceField)) return acc;
- acc[targetKey] = sourceField;
- return acc;
- },
- {} as Record<string, string>,
- );
- if (!Object.keys(nextMapping).length) return false;
- fieldMapping.value = { ...fieldMapping.value, ...nextMapping };
- return true;
- };
- const filteredOriginalOptions = computed(() => {
- if (!originFilterKeyword.value) return originalFieldOptions.value;
- return originalFieldOptions.value.filter((item) => item.label.includes(originFilterKeyword.value));
- });
- const resolveRecognitionBasicInfoTargetKey = (
- targetField: unknown,
- fields: MappingTargetField[] = fixedTargetFields.value,
- ) => {
- const rawTargetField = normalizeRecognitionConfigText(targetField);
- if (!rawTargetField || !fields.length) return '';
- const matchedByKey = fields.find((item) => item.key === rawTargetField);
- if (matchedByKey) return matchedByKey.key;
- const matchedByLabel = fields.find((item) => item.label === rawTargetField);
- if (matchedByLabel) return matchedByLabel.key;
- const normalizedTargetField = normalizeFieldMatchText(rawTargetField);
- if (!normalizedTargetField) return '';
- const matchedByNormalized = fields.find(
- (item) =>
- normalizeFieldMatchText(item.key) === normalizedTargetField ||
- normalizeFieldMatchText(item.label) === normalizedTargetField,
- );
- if (matchedByNormalized) return matchedByNormalized.key;
- const matchedByFuzzy = fields.find((item) => {
- const normalizedKey = normalizeFieldMatchText(item.key);
- const normalizedLabel = normalizeFieldMatchText(item.label);
- return (
- (!!normalizedKey &&
- (normalizedKey.includes(normalizedTargetField) || normalizedTargetField.includes(normalizedKey))) ||
- (!!normalizedLabel &&
- (normalizedLabel.includes(normalizedTargetField) || normalizedTargetField.includes(normalizedLabel)))
- );
- });
- return matchedByFuzzy?.key || '';
- };
- const buildRecognitionBasicInfoMatchedTargetKeySet = (
- rows: RecognitionBasicInfoRow[] = [],
- fields: MappingTargetField[] = fixedTargetFields.value,
- ) =>
- rows.reduce((acc, item) => {
- if (!normalizeRecognitionConfigText(item.headerMatch)) return acc;
- const targetKey = resolveRecognitionBasicInfoTargetKey(item.targetField, fields);
- if (targetKey) {
- acc.add(targetKey);
- }
- return acc;
- }, new Set<string>());
- const buildEffectiveFieldMapping = (mapping: Record<string, string>, lockedTargetKeySet: Set<string>) =>
- Object.entries(mapping).reduce(
- (acc, [targetKey, header]) => {
- const mappedHeader = normalizeRuleText(header);
- if (!mappedHeader || lockedTargetKeySet.has(targetKey)) return acc;
- acc[targetKey] = mappedHeader;
- return acc;
- },
- {} as Record<string, string>,
- );
- const recognitionBasicInfoMatchedTargetKeySet = computed(() =>
- buildRecognitionBasicInfoMatchedTargetKeySet(recognitionBasicInfoRows.value, fixedTargetFields.value),
- );
- const isTargetMatchedByRecognitionBasicInfo = (targetKey: string) =>
- recognitionBasicInfoMatchedTargetKeySet.value.has(targetKey);
- const getTargetFieldByKey = (targetKey: string, fields: MappingTargetField[] = fixedTargetFields.value) =>
- fields.find((item) => item.key === targetKey) || null;
- const buildAmountZfHandledTargetKeySet = (
- fields: MappingTargetField[] = fixedTargetFields.value,
- enabled = recognitionAmountZfEnabled.value,
- ) =>
- fields.reduce((acc, field) => {
- if (enabled && isLoanFlagTargetField(field)) {
- acc.add(field.key);
- }
- return acc;
- }, new Set<string>());
- const isTargetHandledByAmountZf = (targetKey: string) => {
- const field = getTargetFieldByKey(targetKey);
- return !!field && recognitionAmountZfEnabled.value && isLoanFlagTargetField(field);
- };
- const recognitionRuleHandledTargetKeySet = computed(() => {
- const nextSet = new Set(recognitionBasicInfoMatchedTargetKeySet.value);
- buildAmountZfHandledTargetKeySet(fixedTargetFields.value, recognitionAmountZfEnabled.value).forEach((key) => {
- nextSet.add(key);
- });
- return nextSet;
- });
- const isTargetHandledByRecognitionRule = (targetKey: string) =>
- recognitionRuleHandledTargetKeySet.value.has(targetKey);
- const effectiveFieldMapping = computed(() =>
- buildEffectiveFieldMapping(fieldMapping.value, recognitionRuleHandledTargetKeySet.value),
- );
- const getDisplayedMappedHeader = (targetKey: string) =>
- isTargetHandledByRecognitionRule(targetKey) ? '' : fieldMapping.value[targetKey] || '';
- const isTargetMatched = (targetKey: string) =>
- isTargetHandledByRecognitionRule(targetKey) || !!effectiveFieldMapping.value[targetKey];
- const resolveMappedTargetKeyByHeader = (header: string) => {
- const normalizedHeader = normalizeRuleText(header);
- if (!normalizedHeader) return '';
- return (
- fixedTargetFields.value.find((item) => normalizeRuleText(getDisplayedMappedHeader(item.key)) === normalizedHeader)
- ?.key || ''
- );
- };
- const getPreviewColumnTarget = (colIndex: number) => {
- const container = tableMockRef.value;
- if (!container) return null;
- return (
- container.querySelector<HTMLElement>(`.mock-row > span[data-preview-col="${colIndex}"]`) ||
- container.querySelector<HTMLElement>(`.mock-match-row > span[data-preview-col="${colIndex}"]`) ||
- container.querySelector<HTMLElement>(`.mock-head > span[data-preview-col="${colIndex}"]`) ||
- null
- );
- };
- const isVisibleInDom = (element: HTMLElement) => {
- return element.offsetParent !== null || element.getClientRects().length > 0;
- };
- const getVisibleOriginOptionList = (targetKey: string) => {
- const lists = Array.from(
- document.querySelectorAll<HTMLElement>(
- `.origin-field-popover .origin-pop-list[data-origin-list-target-key="${targetKey}"]`,
- ),
- );
- return lists.find((item) => isVisibleInDom(item)) || lists[0] || null;
- };
- const scrollOriginOptionIntoView = (targetKey: string) => {
- const optionValue = normalizeRuleText(getDisplayedMappedHeader(targetKey));
- if (!optionValue) return;
- void nextTick(() => {
- const tryScroll = (attempt = 0) => {
- const list = getVisibleOriginOptionList(targetKey);
- if (!list) {
- if (attempt < 6) {
- window.requestAnimationFrame(() => tryScroll(attempt + 1));
- }
- return;
- }
- const options = Array.from(
- list.querySelectorAll<HTMLElement>(`[data-origin-target-key="${targetKey}"][data-origin-option-value]`),
- );
- const option =
- options.find((item) => normalizeRuleText(item.dataset.originOptionValue) === optionValue) || null;
- const scrollTarget = option?.closest<HTMLElement>('.origin-tag-wrap') || option;
- scrollTarget?.scrollIntoView({
- block: 'nearest',
- inline: 'nearest',
- behavior: 'smooth',
- });
- };
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => {
- tryScroll();
- });
- });
- });
- };
- const scrollPreviewColumnIntoView = (colIndex: number) => {
- if (colIndex < 0) return;
- void nextTick(() => {
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => {
- const container = tableMockRef.value;
- const target = getPreviewColumnTarget(colIndex);
- if (!container || !target) return;
- const containerRect = container.getBoundingClientRect();
- const targetRect = target.getBoundingClientRect();
- const targetLeft = container.scrollLeft + (targetRect.left - containerRect.left);
- const desiredLeft = targetLeft - mockIndexColumnWidth - 16;
- const maxScrollLeft = Math.max(container.scrollWidth - container.clientWidth, 0);
- container.scrollTo({
- left: Math.min(Math.max(desiredLeft, 0), maxScrollLeft),
- behavior: 'smooth',
- });
- });
- });
- });
- };
- const applyPreviewColumnState = (
- header: string,
- colIndex: number,
- options: {
- resetSelectedRow?: boolean;
- activeTargetKey?: string;
- } = {},
- ) => {
- if (colIndex < 0) return;
- currentColumn.value = colIndex;
- if (options.resetSelectedRow ?? true) {
- selectedRowIndex.value = null;
- }
- selectedAddedColumn.value = addedColumns.value.includes(header) ? header : '';
- activeMappingTargetKey.value =
- options.activeTargetKey !== undefined ? options.activeTargetKey : resolveMappedTargetKeyByHeader(header);
- scrollPreviewColumnIntoView(colIndex);
- };
- const capturePreviewColumnSnapshot = (): PreviewColumnSnapshot => ({
- colIndex: currentColumn.value,
- selectedRowIndex: selectedRowIndex.value,
- selectedAddedColumn: selectedAddedColumn.value,
- activeTargetKey: activeMappingTargetKey.value,
- });
- const clearOriginHoverPreviewState = () => {
- hoveredOriginOptionTargetKey.value = '';
- hoveredOriginOptionValue.value = '';
- originHoverPreviewSnapshot.value = null;
- };
- const restoreOriginHoverPreview = (targetKey?: string) => {
- if (targetKey && hoveredOriginOptionTargetKey.value && hoveredOriginOptionTargetKey.value !== targetKey) {
- return;
- }
- const snapshot = originHoverPreviewSnapshot.value;
- clearOriginHoverPreviewState();
- if (!snapshot) return;
- currentColumn.value = snapshot.colIndex;
- selectedRowIndex.value = snapshot.selectedRowIndex;
- selectedAddedColumn.value = snapshot.selectedAddedColumn;
- activeMappingTargetKey.value = snapshot.activeTargetKey;
- if (snapshot.colIndex !== null && snapshot.colIndex >= 0) {
- scrollPreviewColumnIntoView(snapshot.colIndex);
- }
- };
- const focusPreviewColumn = (
- header: string,
- colIndex: number,
- options: {
- resetSelectedRow?: boolean;
- activeTargetKey?: string;
- } = {},
- ) => {
- applyPreviewColumnState(header, colIndex, options);
- };
- const syncMappingSelectionState = (targetKey: string) => {
- activeMappingTargetKey.value = targetKey;
- const mappedHeader = normalizeRuleText(getDisplayedMappedHeader(targetKey));
- if (mappedHeader) {
- const colIndex = rightTableHeaders.value.findIndex((header) => normalizeRuleText(header) === mappedHeader);
- if (colIndex >= 0) {
- focusPreviewColumn(rightTableHeaders.value[colIndex], colIndex, {
- resetSelectedRow: true,
- activeTargetKey: targetKey,
- });
- return;
- }
- }
- const targetLabel = normalizeFieldMatchText(
- fixedTargetFields.value.find((item) => item.key === targetKey)?.label || '',
- );
- if (!targetLabel) return;
- const fallbackColIndex = rightTableHeaders.value.findIndex(
- (header) => normalizeFieldMatchText(header) === targetLabel,
- );
- if (fallbackColIndex < 0) return;
- focusPreviewColumn(rightTableHeaders.value[fallbackColIndex], fallbackColIndex, {
- resetSelectedRow: true,
- activeTargetKey: targetKey,
- });
- };
- const handleMappingRowSelect = (targetKey: string) => {
- syncMappingSelectionState(targetKey);
- if (!isTargetHandledByRecognitionRule(targetKey)) {
- mappingPopoverOpenKey.value = targetKey;
- originFilterKeyword.value = '';
- scrollOriginOptionIntoView(targetKey);
- }
- };
- const handleOriginOptionHover = (targetKey: string, fieldValue: string) => {
- const normalizedFieldValue = normalizeRuleText(fieldValue);
- if (!normalizedFieldValue) return;
- if (!originHoverPreviewSnapshot.value || hoveredOriginOptionTargetKey.value !== targetKey) {
- originHoverPreviewSnapshot.value = capturePreviewColumnSnapshot();
- }
- hoveredOriginOptionTargetKey.value = targetKey;
- hoveredOriginOptionValue.value = fieldValue;
- const colIndex = rightTableHeaders.value.findIndex((header) => normalizeRuleText(header) === normalizedFieldValue);
- if (colIndex < 0) return;
- applyPreviewColumnState(rightTableHeaders.value[colIndex], colIndex, {
- resetSelectedRow: true,
- activeTargetKey: targetKey,
- });
- };
- const handleOriginOptionHoverLeave = (targetKey: string) => {
- restoreOriginHoverPreview(targetKey);
- };
- const isOriginOptionHovering = (targetKey: string, fieldValue: string) => {
- return (
- hoveredOriginOptionTargetKey.value === targetKey &&
- normalizeRuleText(hoveredOriginOptionValue.value) === normalizeRuleText(fieldValue)
- );
- };
- const isFieldTakenByOther = (fieldValue: string, currentKey: string) => {
- return Object.entries(effectiveFieldMapping.value).some(
- ([key, value]) => key !== currentKey && value === fieldValue,
- );
- };
- const handleMappingPopoverOpen = (key: string, open: boolean) => {
- if (isTargetHandledByRecognitionRule(key)) {
- mappingPopoverOpenKey.value = '';
- originFilterKeyword.value = '';
- restoreOriginHoverPreview(key);
- return;
- }
- mappingPopoverOpenKey.value = open ? key : '';
- if (open) {
- handleMappingRowSelect(key);
- }
- if (!open) {
- restoreOriginHoverPreview(key);
- originFilterKeyword.value = '';
- }
- };
- const handleMappingTriggerClick = (targetKey: string) => {
- if (isTargetHandledByRecognitionRule(targetKey)) {
- activeMappingTargetKey.value = targetKey;
- mappingPopoverOpenKey.value = '';
- originFilterKeyword.value = '';
- return;
- }
- handleMappingRowSelect(targetKey);
- mappingPopoverOpenKey.value = targetKey;
- };
- const closeMappingPopoverPreview = () => {
- mappingPopoverOpenKey.value = '';
- originFilterKeyword.value = '';
- clearOriginHoverPreviewState();
- };
- const setFieldBlank = (targetKey: string) => {
- if (isTargetHandledByRecognitionRule(targetKey)) return;
- restoreOriginHoverPreview(targetKey);
- activeMappingTargetKey.value = targetKey;
- fieldMapping.value[targetKey] = '';
- refreshRecognitionDirectionState(activeNodeInfo.value);
- mappingPopoverOpenKey.value = '';
- };
- const selectOriginalField = (targetKey: string, fieldValue: string) => {
- if (isTargetHandledByRecognitionRule(targetKey)) return;
- if (isFieldTakenByOther(fieldValue, targetKey)) return;
- if (normalizeRuleText(getDisplayedMappedHeader(targetKey)) === normalizeRuleText(fieldValue)) {
- setFieldBlank(targetKey);
- return;
- }
- clearOriginHoverPreviewState();
- fieldMapping.value[targetKey] = fieldValue;
- refreshRecognitionDirectionState(activeNodeInfo.value);
- mappingPopoverOpenKey.value = '';
- syncMappingSelectionState(targetKey);
- };
- const replaceOriginalField = (targetKey: string, fieldValue: string) => {
- if (isTargetHandledByRecognitionRule(targetKey)) return;
- clearOriginHoverPreviewState();
- Object.keys(effectiveFieldMapping.value).forEach((key) => {
- if (key !== targetKey && effectiveFieldMapping.value[key] === fieldValue) {
- fieldMapping.value[key] = '';
- }
- });
- fieldMapping.value[targetKey] = fieldValue;
- refreshRecognitionDirectionState(activeNodeInfo.value);
- mappingPopoverOpenKey.value = '';
- syncMappingSelectionState(targetKey);
- };
- const childTemplateName = ref('');
- const showChildTemplateExistsWarning = (name: string) => {
- Modal.warning({
- title: '子模板名称已存在',
- content: `子模板名称“${name}”已存在,请修改后再导入数据。`,
- centered: true,
- });
- };
- const showDuplicateChildTemplateWarning = (names: string[]) => {
- const previewNames = names.slice(0, 3).join('、');
- const suffix = names.length > 3 ? ` 等 ${names.length} 个名称` : '';
- Modal.warning({
- title: '子模板名称重复',
- content: `当前清洗结果中存在重复的子模板名称:${previewNames}${suffix},请修改后再导入数据。`,
- centered: true,
- });
- };
- const checkTemplateNameAvailable = async (tableNameCn: string) => {
- const response = await tplCheckTableName('', tableNameCn);
- // const available = resolveTemplateNameCheckResult(response);
- // if (available === null) {
- // throw new Error('模板名称校验结果无法识别');
- // }
- return response === 'N';
- };
- const handleChildTemplateNameBlur = async () => {
- const tableNameCn = normalizeRuleText(childTemplateName.value);
- if (!tableNameCn) return;
- try {
- const available = await checkTemplateNameAvailable(tableNameCn);
- if (!available) {
- showChildTemplateExistsWarning(tableNameCn);
- }
- } catch (error) {
- createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
- }
- };
- const collectChildTemplateNames = (nodes: TreeFileInfo[] = []) => {
- const names: string[] = [];
- const walk = (items: TreeFileInfo[] = []) => {
- items.forEach((item) => {
- const childName = normalizeRuleText(item.childTemplateName);
- if (childName) {
- names.push(childName);
- }
- if (Array.isArray(item.children) && item.children.length) {
- walk(item.children);
- }
- });
- };
- walk(nodes);
- return names;
- };
- const findDuplicateChildTemplateNames = (names: string[]) => {
- const duplicateNameMap = new Map<string, string>();
- const counter = new Map<string, number>();
- names.forEach((name) => {
- const key = normalizeTemplateNameKey(name);
- if (!key) return;
- const nextCount = (counter.get(key) || 0) + 1;
- counter.set(key, nextCount);
- if (nextCount > 1 && !duplicateNameMap.has(key)) {
- duplicateNameMap.set(key, name);
- }
- });
- return Array.from(duplicateNameMap.values());
- };
- const ensureChildTemplateNamesAvailable = async (nodes: TreeFileInfo[] = []) => {
- const childTemplateNames = collectChildTemplateNames(nodes);
- if (!childTemplateNames.length) return true;
- const duplicateNames = findDuplicateChildTemplateNames(childTemplateNames);
- if (duplicateNames.length) {
- showDuplicateChildTemplateWarning(duplicateNames);
- return false;
- }
- const uniqueNameMap = new Map<string, string>();
- childTemplateNames.forEach((name) => {
- const key = normalizeTemplateNameKey(name);
- if (key && !uniqueNameMap.has(key)) {
- uniqueNameMap.set(key, name);
- }
- });
- for (const name of uniqueNameMap.values()) {
- const available = await checkTemplateNameAvailable(name);
- if (!available) {
- showChildTemplateExistsWarning(name);
- return false;
- }
- }
- return true;
- };
- const cleaningRulePanelExpanded = ref(false);
- const recognitionPanelExpanded = ref(false);
- const activeRecognitionRule = ref('');
- const recognitionDirectionFields = ref<RecognitionDirectionField[]>([]);
- const recognitionBasicInfoBaseFuncRegx = ref('');
- const recognitionBasicInfoManualVisible = ref(false);
- const recognitionBasicInfoExpanded = ref(false);
- const recognitionBasicInfoRows = ref<RecognitionBasicInfoRow[]>([]);
- const recognitionBasicInfoRegex = /^\{(.*?)\}$/s;
- const fixedRecognitionBasicInfoFieldBindings = {
- personCertNo: '客户证件号码',
- personName: '客户名称',
- personCardNo: '本方卡号',
- } as const;
- const normalizedFixedRecognitionBasicInfoFieldMap = Object.entries(fixedRecognitionBasicInfoFieldBindings).reduce(
- (acc, [fieldKey, targetField]) => {
- const normalizedFieldKey = normalizeFieldMatchText(fieldKey);
- const normalizedTargetField = normalizeFieldMatchText(targetField);
- if (normalizedFieldKey) {
- acc[normalizedFieldKey] = targetField;
- }
- if (normalizedTargetField) {
- acc[normalizedTargetField] = targetField;
- }
- return acc;
- },
- {} as Record<string, string>,
- );
- const requiredTotal = computed(() => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item)).length);
- const requiredMatchedCount = computed(
- () => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item) && isTargetMatched(item.key)).length,
- );
- const matchedFieldCount = computed(() => fixedTargetFields.value.filter((item) => isTargetMatched(item.key)).length);
- const showRecognitionBasicInfoPanel = computed(
- () => !!recognitionBasicInfoBaseFuncRegx.value || recognitionBasicInfoRows.value.length > 0,
- );
- const parseRecognitionDirectionEntries = (value: unknown): RecognitionDirectionItem[] => {
- const rawText = normalizeRecognitionConfigText(value);
- if (!rawText) return [];
- try {
- let parsedValue: Record<string, any> | null = null;
- try {
- parsedValue = JSON.parse(rawText);
- } catch (jsonError) {
- void jsonError;
- const normalizedText = rawText
- .replace(/([{,]\s*)'([^']+?)'\s*:/g, '$1"$2":')
- .replace(/:\s*'([^']*?)'(\s*[,}])/g, ': "$1"$2');
- parsedValue = JSON.parse(normalizedText);
- }
- if (!parsedValue || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) return [];
- return Object.entries(parsedValue).reduce((acc, [label, itemValue], index) => {
- const resolvedLabel = String(label || '').trim();
- if (!resolvedLabel) return acc;
- acc.push({
- key: `${resolvedLabel}_${index}`,
- label: resolvedLabel,
- value: itemValue === null || itemValue === undefined ? '' : String(itemValue),
- });
- return acc;
- }, [] as RecognitionDirectionItem[]);
- } catch (error) {
- console.error('解析 directionConf 失败', error);
- return [];
- }
- };
- const buildRecognitionDirectionConf = (entries?: RecognitionDirectionItem[]) => {
- if (!entries?.length) return '';
- const payload = entries.reduce(
- (acc, item) => {
- const label = normalizeRecognitionConfigText(item.label);
- if (!label) return acc;
- acc[label] = normalizeRecognitionConfigText(item.value);
- return acc;
- },
- {} as Record<string, string>,
- );
- return Object.keys(payload).length ? JSON.stringify(payload) : '';
- };
- const resolveRecognitionDirectionTab = (
- fieldLabel: string,
- entries: RecognitionDirectionItem[] = [],
- ): RecognitionRuleTabKey | '' => {
- const mergedText = `${fieldLabel}|${entries.map((item) => item.label).join('|')}`;
- if (fieldLabel.includes('借贷') || /(收入|支出|借方|贷方)/.test(mergedText)) {
- return '借贷标志';
- }
- if (/(通话|短信|通信|话单)/.test(fieldLabel) || /(主叫|被叫|发送|接收|呼入|呼出|上行|下行)/.test(mergedText)) {
- return '通信类别';
- }
- return '';
- };
- const resolveRecognitionDirectionFields = (
- node?: TreeFileInfo | null,
- options: { preferTemplate?: boolean; currentFields?: RecognitionDirectionField[] } = {},
- ) => {
- const currentFieldMap = (options.currentFields || []).reduce(
- (acc, item) => {
- acc[item.key] = item;
- return acc;
- },
- {} as Record<string, RecognitionDirectionField>,
- );
- const tableRuleMap = normalizeTableRuleList(node?.tableRuleList).reduce(
- (acc, item, index) => {
- acc[getTableRuleTargetKey(item, index)] = item;
- return acc;
- },
- {} as Record<string, TreeTableRuleInfo>,
- );
- return fixedTargetFields.value.reduce((acc, field, index) => {
- const templateFieldRecord = (resolveTemplateFieldMeta(field, index) || {}) as Record<string, any>;
- const ruleFieldRecord = tableRuleMap[field.key] || null;
- const currentFieldRecord = currentFieldMap[field.key];
- const templateDirectionConf = normalizeRecognitionConfigText(
- templateFieldRecord.directionConf ?? templateFieldRecord.direction_conf,
- );
- const ruleDirectionConf = normalizeRecognitionConfigText(ruleFieldRecord?.directionConf);
- const currentDirectionConf = currentFieldRecord?.entries?.length
- ? buildRecognitionDirectionConf(currentFieldRecord.entries)
- : '';
- const directionConfText = currentDirectionConf
- ? currentDirectionConf
- : options.preferTemplate
- ? templateDirectionConf
- : ruleDirectionConf || templateDirectionConf;
- const baseEntries = currentFieldRecord?.entries?.length
- ? currentFieldRecord.entries.map((entry) => ({ ...entry }))
- : parseRecognitionDirectionEntries(directionConfText);
- const baseTab = currentFieldRecord?.tab || resolveRecognitionDirectionTab(field.label, baseEntries);
- const entries = baseTab
- ? buildRecognitionDirectionEntriesByMappedColumn(field, baseTab, baseEntries)
- : baseEntries;
- const tab = currentFieldRecord?.tab || resolveRecognitionDirectionTab(field.label, entries);
- if (!entries.length || !tab) return acc;
- acc.push({
- key: field.key,
- label: field.label,
- tab,
- entries,
- });
- return acc;
- }, [] as RecognitionDirectionField[]);
- };
- const applyRecognitionDirectionState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
- recognitionDirectionFields.value = resolveRecognitionDirectionFields(node, options);
- };
- const activeRecognitionDirectionFields = computed(() =>
- recognitionDirectionFields.value.filter((item) => item.tab === activeRecognitionRule.value),
- );
- const hasRecognitionLoanTab = computed(() =>
- recognitionDirectionFields.value.some((item) => item.entries.length && item.tab === '借贷标志'),
- );
- const hasRecognitionCommunicationTab = computed(() =>
- recognitionDirectionFields.value.some((item) => item.entries.length && item.tab === '通信类别'),
- );
- const splitRecognitionDirectionKeywords = (value?: string) =>
- normalizeRecognitionConfigText(value)
- .split(/[\n,,;;、]+/)
- .map((item) => item.trim())
- .filter(Boolean);
- const normalizeRecognitionDirectionGroup = (value: string, tab: RecognitionRuleTabKey) => {
- if (tab === '借贷标志') {
- return normalizeLoanFlagCoreValue(value);
- }
- const normalized = normalizeCallTypeCoreValue(value);
- if (['主叫', '发送', '呼出', '上行'].includes(normalized)) {
- return '主叫';
- }
- if (['被叫', '接收', '呼入', '下行'].includes(normalized)) {
- return '被叫';
- }
- return '';
- };
- const mergeRecognitionDirectionKeywords = (currentValue = '', nextValues: string[] = []) => {
- const mergedValues: string[] = [];
- const normalizedValueSet = new Set<string>();
- [...splitRecognitionDirectionKeywords(currentValue), ...nextValues].forEach((item) => {
- const text = normalizeRecognitionConfigText(item);
- const normalizedText = normalizeFieldMatchText(text);
- if (!text || !normalizedText || normalizedValueSet.has(normalizedText)) return;
- normalizedValueSet.add(normalizedText);
- mergedValues.push(text);
- });
- return mergedValues.join(',');
- };
- const getRecognitionDirectionSampleValues = (fieldKey: string) => {
- const mappedHeader = normalizeRuleText(effectiveFieldMapping.value[fieldKey] || fieldMapping.value[fieldKey]);
- if (!mappedHeader) return [] as string[];
- const matchedHeader = rightTableHeaders.value.find((item) => normalizeRuleText(item) === mappedHeader);
- if (!matchedHeader) return [] as string[];
- const collectedValues: string[] = [];
- const normalizedValueSet = new Set<string>();
- sourceTableRows.value.forEach((row) => {
- const rawValue = normalizeRecognitionConfigText(row?.[matchedHeader]);
- const normalizedValue = normalizeFieldMatchText(rawValue);
- if (!rawValue || !normalizedValue || normalizedValueSet.has(normalizedValue)) return;
- normalizedValueSet.add(normalizedValue);
- collectedValues.push(rawValue);
- });
- return collectedValues;
- };
- const buildRecognitionDirectionEntriesByMappedColumn = (
- field: Pick<RecognitionDirectionField, 'key' | 'label'>,
- tab: RecognitionRuleTabKey,
- entries: RecognitionDirectionItem[] = [],
- ) => {
- const baseEntries = entries.map((item, index) => ({
- key: item.key || `${field.key}_${index}`,
- label: normalizeRecognitionConfigText(item.label),
- value: normalizeRecognitionConfigText(item.value),
- }));
- if (baseEntries.some((item) => item.label)) {
- return baseEntries.filter((item) => item.label);
- }
- const sampleValues = getRecognitionDirectionSampleValues(field.key);
- if (!sampleValues.length) {
- return baseEntries.filter((item) => item.label);
- }
- const groupedKeywordMap = sampleValues.reduce(
- (acc, value) => {
- const groupLabel = normalizeRecognitionDirectionGroup(value, tab);
- if (!groupLabel) return acc;
- const coreKeyword = tab === '借贷标志' ? normalizeLoanFlagCoreValue(value) : normalizeCallTypeCoreValue(value);
- const nextKeywords = [coreKeyword, value].filter((item): item is string => !!item);
- acc[groupLabel] = mergeRecognitionDirectionKeywords((acc[groupLabel] || []).join(','), nextKeywords)
- .split(/[\n,,;;、]+/)
- .map((item) => item.trim())
- .filter(Boolean);
- return acc;
- },
- {} as Record<string, string[]>,
- );
- if (!Object.keys(groupedKeywordMap).length) {
- return baseEntries.filter((item) => item.label);
- }
- const nextEntries = Object.keys(groupedKeywordMap).map((label, index) => ({
- key: `${field.key}_${index}`,
- label,
- value: '',
- }));
- const groupEntryIndexMap = nextEntries.reduce(
- (acc, item, index) => {
- const groupLabel =
- normalizeRecognitionDirectionGroup(item.label, tab) || normalizeRecognitionConfigText(item.label);
- if (groupLabel && acc[groupLabel] === undefined) {
- acc[groupLabel] = index;
- }
- return acc;
- },
- {} as Record<string, number>,
- );
- Object.entries(groupedKeywordMap).forEach(([groupLabel, keywords]) => {
- let targetIndex = groupEntryIndexMap[groupLabel];
- if (targetIndex === undefined) {
- targetIndex = nextEntries.length;
- nextEntries.push({
- key: `${field.key}_${targetIndex}`,
- label: groupLabel,
- value: '',
- });
- groupEntryIndexMap[groupLabel] = targetIndex;
- }
- nextEntries[targetIndex].value = mergeRecognitionDirectionKeywords(nextEntries[targetIndex].value, keywords);
- });
- return nextEntries.filter((item) => item.label);
- };
- const buildRecognitionDirectionComparableSet = (
- fields: RecognitionDirectionField[],
- ): RecognitionDirectionComparableMap => {
- const initialSet = {
- 借贷标志: new Set<string>(),
- 通信类别: new Set<string>(),
- };
- fields.forEach((field) => {
- const currentSet = initialSet[field.tab];
- field.entries.forEach((entry) => {
- [entry.label, ...splitRecognitionDirectionKeywords(entry.value)].forEach((keyword) => {
- const normalizedKeyword = normalizeRecognitionConfigText(keyword);
- if (normalizedKeyword) {
- currentSet.add(normalizedKeyword);
- }
- });
- });
- });
- return initialSet;
- };
- const recognitionDirectionComparableSetByTab = computed<RecognitionDirectionComparableMap>(() =>
- buildRecognitionDirectionComparableSet(recognitionDirectionFields.value),
- );
- const recognitionDirectionComparableSetByFieldKey = computed<Record<string, RecognitionDirectionComparableMap>>(() =>
- recognitionDirectionFields.value.reduce(
- (acc, field) => {
- acc[field.key] = buildRecognitionDirectionComparableSet([field]);
- return acc;
- },
- {} as Record<string, RecognitionDirectionComparableMap>,
- ),
- );
- const matchRecognitionDirectionConfigValue = (value: string, tab: RecognitionRuleTabKey, targetKey = '') => {
- const keywordSet =
- (targetKey ? recognitionDirectionComparableSetByFieldKey.value[targetKey]?.[tab] : undefined) ||
- recognitionDirectionComparableSetByTab.value[tab];
- if (!keywordSet.size) return null;
- const normalizedValue = normalizeRecognitionConfigText(value);
- if (normalizedValue && keywordSet.has(normalizedValue)) {
- return true;
- }
- return false;
- };
- const recognitionHeaderPreviewText = computed(() => {
- if (headerRowIndex.value === null) {
- return '未设置表头行,暂无法分析表头上方内容。';
- }
- const previewHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
- const previewRows = sourceTableRows.value.slice(0, headerRowIndex.value);
- const content = previewRows
- .map((row) =>
- previewHeaders
- .map((header) => normalizeSingleLineText(row[header]))
- .filter(Boolean)
- .join(''),
- )
- .filter(Boolean)
- .join('');
- return content || '当前表头行上方暂无可识别内容。';
- });
- const recognitionHeaderPreviewMeta = computed(() => {
- if (headerRowIndex.value === null) return '待识别';
- if (headerRowIndex.value <= 0) return '上方 0 行';
- return `上方 ${headerRowIndex.value} 行`;
- });
- const normalizeRecognitionBasicInfoText = (value: unknown) => normalizeRecognitionConfigText(value).trim();
- const stripRecognitionBasicInfoTrailingColon = (value: string) => value.replace(/[::]\s*$/g, '').trim();
- const normalizeRecognitionBasicInfoStorageText = (value: unknown) =>
- stripRecognitionBasicInfoTrailingColon(normalizeRecognitionBasicInfoText(value));
- const resolveRecognitionBasicInfoTargetField = (value: unknown) => {
- const normalizedValue = normalizeRecognitionBasicInfoStorageText(value);
- if (!normalizedValue) return '';
- return normalizedFixedRecognitionBasicInfoFieldMap[normalizeFieldMatchText(normalizedValue)] || normalizedValue;
- };
- const parseRecognitionBasicInfoPayload = (funcRegx: unknown) => {
- const rawText = normalizeRecognitionBasicInfoText(funcRegx);
- if (!rawText) return null;
- const matchedText = rawText.match(recognitionBasicInfoRegex)?.[0];
- if (!matchedText) return null;
- try {
- const parsedValue = JSON.parse(matchedText) as Record<string, any>;
- if (!parsedValue || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
- return null;
- }
- return parsedValue;
- } catch (error) {
- console.error('解析基本信息配置失败', error);
- return null;
- }
- };
- const buildRecognitionBasicInfoTargetFieldByHeaderMatchMap = (funcRegx: unknown) => {
- const parsedValue = parseRecognitionBasicInfoPayload(funcRegx);
- if (!parsedValue) return {} as Record<string, string>;
- return Object.entries(parsedValue).reduce(
- (acc, [targetField, rawValue]) => {
- const rawList = Array.isArray(rawValue) ? rawValue : [];
- const headerMatch = normalizeRecognitionBasicInfoText(rawList[0] ?? '');
- const normalizedHeaderMatch = normalizeRecognitionBasicInfoStorageText(headerMatch);
- if (normalizedHeaderMatch) {
- acc[normalizedHeaderMatch] = resolveRecognitionBasicInfoTargetField(targetField);
- }
- return acc;
- },
- {} as Record<string, string>,
- );
- };
- const parseRecognitionBasicInfoRows = (funcRegx: unknown, referenceFuncRegx?: unknown): RecognitionBasicInfoRow[] => {
- const parsedValue = parseRecognitionBasicInfoPayload(funcRegx);
- if (!parsedValue) return [];
- const parentTargetFieldMap = buildRecognitionBasicInfoTargetFieldByHeaderMatchMap(referenceFuncRegx);
- return Object.entries(parsedValue).reduce((acc, [storedKey, rawValue]) => {
- const rawList = Array.isArray(rawValue) ? rawValue : [];
- const headerMatch = normalizeRecognitionBasicInfoText(rawList[0] ?? '');
- const normalizedHeaderMatch = normalizeRecognitionBasicInfoStorageText(headerMatch);
- const endFlag = normalizeRecognitionBasicInfoText(rawList[1] ?? '');
- const normalizedStoredKey = normalizeRecognitionBasicInfoStorageText(storedKey);
- const resolvedStoredTargetField = resolveRecognitionBasicInfoTargetField(normalizedStoredKey);
- const mappedParentTargetField =
- normalizedHeaderMatch && normalizedStoredKey === normalizedHeaderMatch
- ? parentTargetFieldMap[normalizedHeaderMatch]
- : '';
- const resolvedTargetField = mappedParentTargetField || resolvedStoredTargetField;
- acc.push({
- key: createRecognitionBasicInfoKey(),
- headerMatch,
- targetField: resolvedTargetField,
- endFlag,
- });
- return acc;
- }, [] as RecognitionBasicInfoRow[]);
- };
- const clearRecognitionBasicInfoFilledContent = (rows: RecognitionBasicInfoRow[]) =>
- rows.map((item) => ({
- ...item,
- headerMatch: '',
- endFlag: '',
- }));
- const hasRecognitionBasicInfoFilledContent = (rows: RecognitionBasicInfoRow[]) =>
- rows.some(
- (item) =>
- !!normalizeRecognitionBasicInfoText(item.headerMatch) || !!normalizeRecognitionBasicInfoText(item.endFlag),
- );
- const hasOwnRecognitionBasicInfoConfig = (node?: TreeFileInfo | null) =>
- !!node && !!normalizeRecognitionConfigText(node.funcRegx);
- const isSameRecognitionBasicInfoConfig = (left?: unknown, right?: unknown) => {
- const leftText = normalizeRecognitionConfigText(left);
- const rightText = normalizeRecognitionConfigText(right);
- return !!leftText && !!rightText && leftText === rightText;
- };
- const resolveRecognitionBasicInfoSource = (
- node?: TreeFileInfo | null,
- options: { preferTemplate?: boolean } = {},
- ) => {
- const templateFuncRegx = normalizeRecognitionConfigText(
- templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
- );
- const hasNodeFuncRegx = hasOwnRecognitionBasicInfoConfig(node);
- const nodeFuncRegx = normalizeRecognitionConfigText(node?.funcRegx);
- if (options.preferTemplate) {
- return templateFuncRegx;
- }
- if (hasNodeFuncRegx) {
- return nodeFuncRegx;
- }
- return templateFuncRegx;
- };
- const applyRecognitionBasicInfoState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
- const templateId = normalizeTemplateId(
- node?.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
- );
- const parentTemplateId = getSelectedTemplateMainId(node?.mainId);
- const parentTemplate = parentTemplateId
- ? resolveTemplateInfoById(parentTemplateId)
- : resolveParentTemplateInfoById(templateId);
- const source = resolveRecognitionBasicInfoSource(node, options);
- const hasNodeFuncRegx =
- hasOwnRecognitionBasicInfoConfig(node) &&
- !isSameRecognitionBasicInfoConfig(
- node?.funcRegx,
- templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
- ) &&
- !isSameRecognitionBasicInfoConfig(node?.funcRegx, parentTemplate?.funcRegx);
- const shouldClearFilledContent = options.preferTemplate || !hasNodeFuncRegx;
- recognitionBasicInfoBaseFuncRegx.value = source;
- const parsedRows = parseRecognitionBasicInfoRows(source, parentTemplate?.funcRegx);
- recognitionBasicInfoRows.value = shouldClearFilledContent
- ? clearRecognitionBasicInfoFilledContent(parsedRows)
- : parsedRows;
- recognitionBasicInfoExpanded.value = !!recognitionBasicInfoRows.value.length;
- };
- const resolveRecognitionAmountZfEnabled = (
- node?: TreeFileInfo | null,
- options: { preferTemplate?: boolean } = {},
- ) => {
- const tableRule = findTransAmountTableRule(node?.tableRuleList);
- const tableRuleZf = getRecordZfValue(tableRule);
- const templateField = findTransAmountTemplateFieldMeta(templateDetail.value || activeTemplateItem.value);
- const templateZf = getRecordZfValue(templateField);
- if (options.preferTemplate) {
- return templateZf !== undefined
- ? getZfFlag(templateZf)
- : tableRuleZf !== undefined
- ? getZfFlag(tableRuleZf)
- : false;
- }
- return tableRuleZf !== undefined
- ? getZfFlag(tableRuleZf)
- : templateZf !== undefined
- ? getZfFlag(templateZf)
- : false;
- };
- const applyRecognitionAmountZfState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
- recognitionAmountZfEnabled.value = resolveRecognitionAmountZfEnabled(node, options);
- };
- const handleRecognitionAmountZfChange = (checked: unknown) => {
- const enabled = getZfFlag(checked);
- recognitionAmountZfEnabled.value = enabled;
- if (mappingPopoverOpenKey.value && isTargetHandledByAmountZf(mappingPopoverOpenKey.value)) {
- mappingPopoverOpenKey.value = '';
- originFilterKeyword.value = '';
- }
- saveCurrentNodeState();
- runRulePipeline();
- createMessage.success(enabled ? '正负标识识别已开启' : '正负标识识别已关闭');
- };
- const buildRecognitionBasicInfoFuncRegx = () => {
- if (!recognitionBasicInfoRows.value.length) {
- return recognitionBasicInfoBaseFuncRegx.value;
- }
- if (!hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value)) {
- return '';
- }
- const payload = recognitionBasicInfoRows.value.reduce(
- (acc, item) => {
- const targetField = resolveRecognitionBasicInfoTargetField(item.targetField);
- if (!targetField) return acc;
- acc[targetField] = [
- normalizeRecognitionBasicInfoText(item.headerMatch),
- normalizeRecognitionBasicInfoText(item.endFlag),
- ];
- return acc;
- },
- {} as Record<string, string[]>,
- );
- return Object.keys(payload).length ? JSON.stringify(payload) : '';
- };
- const handleClearRecognitionDirection = () => {
- const currentTab = activeRecognitionRule.value as RecognitionRuleTabKey;
- if (!currentTab) return;
- recognitionDirectionFields.value = recognitionDirectionFields.value.map((item) => {
- if (item.tab !== currentTab) return item;
- return {
- ...item,
- entries: item.entries.map((entry) => ({
- ...entry,
- value: '',
- })),
- };
- });
- };
- const handleSaveRecognitionDirection = () => {
- if (!activeRecognitionDirectionFields.value.length) return;
- saveCurrentNodeState();
- runRulePipeline();
- createMessage.success(`${activeRecognitionRule.value}配置已保存`);
- };
- const refreshRecognitionDirectionState = (node?: TreeFileInfo | null) => {
- recognitionDirectionFields.value = resolveRecognitionDirectionFields(node, {
- currentFields: recognitionDirectionFields.value,
- });
- };
- const getMappingStatusClass = (item: Pick<MappingTargetField, 'key' | 'label' | 'required'>) => {
- if (isTargetMatched(item.key)) return 'is-matched';
- if (isMappingTargetRequired(item)) return 'is-required';
- return 'is-optional';
- };
- const getFieldMatchScore = (targetLabel: string, header: string) => {
- const rawTarget = normalizeRuleText(targetLabel);
- const rawHeader = normalizeRuleText(header);
- if (!rawTarget || !rawHeader) return -1;
- if (rawTarget === rawHeader) return 1000;
- const normalizedTarget = normalizeFieldMatchText(rawTarget);
- const normalizedHeader = normalizeFieldMatchText(rawHeader);
- if (!normalizedTarget || !normalizedHeader) return -1;
- if (normalizedTarget === normalizedHeader) return 900;
- return -1;
- };
- const findBestMatchedHeader = (targetLabel: string, headers: string[], usedHeaders: Set<string>) => {
- let bestHeader = '';
- let bestScore = -1;
- headers.forEach((header, index) => {
- if (usedHeaders.has(header)) return;
- const score = getFieldMatchScore(targetLabel, header);
- if (score < 0) return;
- if (score > bestScore) {
- bestScore = score;
- bestHeader = header;
- return;
- }
- if (score === bestScore && bestHeader) {
- const currentDiff = Math.abs(
- normalizeFieldMatchText(bestHeader).length - normalizeFieldMatchText(targetLabel).length,
- );
- const nextDiff = Math.abs(normalizeFieldMatchText(header).length - normalizeFieldMatchText(targetLabel).length);
- if (nextDiff < currentDiff) {
- bestHeader = header;
- return;
- }
- if (nextDiff === currentDiff) {
- const currentIndex = headers.findIndex((item) => item === bestHeader);
- if (currentIndex < 0 || index < currentIndex) {
- bestHeader = header;
- }
- }
- }
- });
- return bestHeader;
- };
- const autoMatchFields = (showMessage = true) => {
- const headers = [...rightTableHeaders.value];
- const usedHeaders = new Set<string>();
- const lockedMapping = Object.entries(fieldMapping.value).reduce(
- (acc, [targetKey, header]) => {
- if (isTargetHandledByRecognitionRule(targetKey) && header) {
- acc[targetKey] = header;
- }
- return acc;
- },
- {} as Record<string, string>,
- );
- const nextMapping = fixedTargetFields.value.reduce((acc, item) => {
- if (isTargetHandledByRecognitionRule(item.key)) {
- return acc;
- }
- const bestHeader = findBestMatchedHeader(item.label, headers, usedHeaders);
- acc[item.key] = bestHeader || '';
- if (bestHeader) {
- usedHeaders.add(bestHeader);
- }
- return acc;
- }, lockedMapping);
- fieldMapping.value = nextMapping;
- refreshRecognitionDirectionState(activeNodeInfo.value);
- if (showMessage) {
- const matchedCount = fixedTargetFields.value.filter(
- (item) => isTargetHandledByRecognitionRule(item.key) || !!nextMapping[item.key],
- ).length;
- createMessage.success(`已完成智能预匹配,共匹配 ${matchedCount} 项`);
- }
- };
- const clearFieldMapping = () => {
- fieldMapping.value = {};
- refreshRecognitionDirectionState(activeNodeInfo.value);
- };
- const applyNodePreset = async (item?: TreeFileInfo | null) => {
- const previewDataset = await loadPreviewDatasetFromFile(item);
- const hasPresetHeaders = previewDataset ? applyPreviewDataset(previewDataset) : applyPresetHeadersFromNode(item);
- restoreStoredColumnRules(item?.tableRuleList);
- applyRecognitionDirectionState(item);
- applyRecognitionBasicInfoState(item);
- applyRecognitionAmountZfState(item);
- childTemplateName.value = normalizeRuleText(item?.childTemplateName);
- const headerRow = normalizeStoredLineNoToRowIndex(item?.lineNo);
- if (headerRow !== null && !hasPresetHeaders) {
- applyHeaderRow(headerRow, { silent: true, autoMatch: false });
- } else {
- headerRowIndex.value = headerRow;
- selectedRowIndex.value = headerRow;
- syncHeaderRowInput();
- }
- if (!applyTableRuleMapping(item?.tableRuleList)) {
- autoMatchFields(false);
- }
- restoreStoredFunRules(item?.tableRuleList);
- restoreFileLevelRules(item);
- runRulePipeline();
- };
- type TemplateTab = 'template' | 'plugin';
- const DEFAULT_TEMPLATE_CATEGORY = '全部分类';
- const FALLBACK_TEMPLATE_CATEGORY = '未分类';
- const templateModalOpen = ref(false);
- const activeTemplateTab = ref<TemplateTab>('template');
- const activeCategory = ref(DEFAULT_TEMPLATE_CATEGORY);
- const templateKeyword = ref('');
- const templateSource = ref<number>(1);
- const templateList = ref<TemplateListItem[]>(mergeTemplateCollections(mockTemplateList));
- const templateCategoryOptions = ref<string[]>([DEFAULT_TEMPLATE_CATEGORY]);
- const activeTemplateItem = ref<TemplateListItem | null>(null);
- const modalTemplateItem = ref<TemplateListItem | null>(null);
- const modalTemplateDetail = ref<TableInfo | null>(null);
- const templatePreviewLoading = ref(false);
- const templateListLoading = ref(false);
- const templateListLoaded = ref(templateList.value.length > 0);
- const templateDetailCache = new Map<number, TableInfo>();
- let templatePreviewRequestToken = 0;
- const templateSourceOptions = [
- { label: '全部', value: 0 },
- { label: '系统内置', value: 1 },
- { label: '新创建子模板', value: 2 },
- ];
- const categories = computed(() => {
- const set = new Set<string>(
- templateCategoryOptions.value.length ? templateCategoryOptions.value : [DEFAULT_TEMPLATE_CATEGORY],
- );
- templateList.value.forEach((t) => set.add(t.classify || FALLBACK_TEMPLATE_CATEGORY));
- return Array.from(set);
- });
- const pluginList = ref<TemplateListItem[]>([
- {
- id: -1,
- tableNameCn: '工行账单特殊处理',
- tableNameEn: 'plug',
- classify: '金融类',
- headers: '插件模板',
- } as TableInfo,
- ]);
- const pluginCategorySet = computed(() => {
- const set = new Set<string>();
- pluginList.value.forEach((item) => {
- const category = normalizeTemplateClassifyText(item.classify) || FALLBACK_TEMPLATE_CATEGORY;
- set.add(category);
- });
- return set;
- });
- const showTemplateTabSwitch = computed(
- () => activeCategory.value === DEFAULT_TEMPLATE_CATEGORY || pluginCategorySet.value.has(activeCategory.value),
- );
- const isPluginTemplateTab = computed(() => activeTemplateTab.value === 'plugin');
- const isSystemTemplateRecord = (record?: Partial<TemplateListItem> | null) => {
- const normalizedValue = String(record?.hasInner ?? '')
- .trim()
- .toUpperCase();
- return ['YES', '1', 'TRUE'].includes(normalizedValue);
- };
- const isChildTemplateRecord = (record?: Partial<TemplateListItem> | null) =>
- !isSystemTemplateRecord(record) && normalizeTemplateId(record?.mainId) > 0;
- const templateSearchBaseList = computed(() => {
- const baseList = activeTemplateTab.value === 'plugin' ? pluginList.value : templateList.value;
- return baseList.filter((tpl) => {
- const passKeyword =
- !templateKeyword.value || `${tpl.tableNameCn || ''}${tpl.tableNameEn || ''}`.includes(templateKeyword.value);
- const passSource =
- templateSource.value === 0 ||
- (templateSource.value === 1 && isSystemTemplateRecord(tpl)) ||
- (templateSource.value === 2 && isChildTemplateRecord(tpl));
- return passKeyword && passSource;
- });
- });
- const filteredTemplateList = computed(() => {
- return templateSearchBaseList.value.filter((tpl) => {
- return (
- activeTemplateTab.value === 'plugin' ||
- activeCategory.value === DEFAULT_TEMPLATE_CATEGORY ||
- (tpl.classify || FALLBACK_TEMPLATE_CATEGORY) === activeCategory.value
- );
- });
- });
- const templateCategoryCountMap = computed<Record<string, number>>(() => {
- return templateSearchBaseList.value.reduce(
- (acc, tpl) => {
- const category = tpl.classify || FALLBACK_TEMPLATE_CATEGORY;
- acc[category] = (acc[category] || 0) + 1;
- acc[DEFAULT_TEMPLATE_CATEGORY] = (acc[DEFAULT_TEMPLATE_CATEGORY] || 0) + 1;
- return acc;
- },
- {} as Record<string, number>,
- );
- });
- const getTemplateDisplayName = (tpl?: Partial<TemplateListItem> | null) => {
- return tpl?.tableNameCn || tpl?.tableNameEn || '未命名模板';
- };
- const getTemplateSourceLabel = (tpl?: Partial<TemplateListItem> | null) => {
- if (Number(tpl?.id) < 0) return '模板插件';
- return isSystemTemplateRecord(tpl) ? '系统内置' : '新创建子模板';
- };
- const getTemplateHeaderList = (tpl?: Partial<TableInfo> | null) => normalizeTemplateHeaders((tpl as any)?.headers);
- const getTemplateHeaderCount = (tpl?: Partial<TableInfo> | null) => getTemplateHeaderList(tpl).length;
- const getCategoryCount = (category: string) => {
- if (category === DEFAULT_TEMPLATE_CATEGORY) return templateCategoryCountMap.value[DEFAULT_TEMPLATE_CATEGORY] || 0;
- return templateCategoryCountMap.value[category] || 0;
- };
- const resolveTemplateClassifyRecords = (payload: unknown): unknown[] => {
- if (Array.isArray(payload)) return payload;
- if (!payload || typeof payload !== 'object') return [];
- const record = payload as Record<string, unknown>;
- for (const key of ['data', 'list', 'records', 'result']) {
- const value = record[key];
- if (Array.isArray(value)) return value;
- }
- return [];
- };
- const normalizeTemplateClassifyValue = (item: unknown) => {
- if (item === null || item === undefined) return '';
- if (typeof item === 'string' || typeof item === 'number') {
- return normalizeTemplateClassifyText(item);
- }
- if (typeof item !== 'object') return '';
- const record = item as Record<string, unknown>;
- return normalizeTemplateClassifyText(
- record.value ?? record.classify ?? record.name ?? record.label ?? record.dictValue ?? record.dictLabel,
- );
- };
- const loadTemplateCategories = async () => {
- try {
- const response = await tplCategoryList();
- const categorySet = new Set<string>([DEFAULT_TEMPLATE_CATEGORY]);
- resolveTemplateClassifyRecords(response).forEach((item) => {
- const value = normalizeTemplateClassifyValue(item);
- if (value && value !== DEFAULT_TEMPLATE_CATEGORY) {
- categorySet.add(value);
- }
- });
- templateCategoryOptions.value = Array.from(categorySet);
- } catch (error) {
- templateCategoryOptions.value = [DEFAULT_TEMPLATE_CATEGORY];
- }
- };
- const getTemplateListItemById = (templateId?: string | number | null) => {
- const normalizedId = normalizeTemplateId(templateId);
- if (!normalizedId) return null;
- return templateList.value.find((item) => normalizeTemplateId(item.id) === normalizedId) || null;
- };
- const applyTableRuleRequiredFlags = (fields: MappingTargetField[], tableRules: TreeTableRuleInfo[]) => {
- if (!tableRules.length) return fields;
- const ruleMapByKey = tableRules.reduce(
- (acc, item, index) => {
- const key = getTableRuleTargetKey(item, index);
- if (key) acc[key] = item;
- return acc;
- },
- {} as Record<string, TreeTableRuleInfo>,
- );
- const ruleMapByLabel = tableRules.reduce(
- (acc, item, index) => {
- const label = getTableRuleTargetLabel(item, index);
- if (label) acc[label] = item;
- return acc;
- },
- {} as Record<string, TreeTableRuleInfo>,
- );
- return fields.map((item) => {
- const rule = ruleMapByKey[item.key] || ruleMapByLabel[item.label];
- return {
- ...item,
- required: rule ? getTableRuleRequiredFlag(rule, item.required) : item.required,
- };
- });
- };
- const resolveNodeTargetFields = (item?: TreeFileInfo | null, preferredTemplateId?: string | number | null) => {
- const tableRules = normalizeTableRuleList(item?.tableRuleList);
- const templateId = normalizeTemplateId(preferredTemplateId ?? item?.templateId);
- const template = (templateId && (getTemplateListItemById(templateId) || mockTemplateMap[templateId])) || null;
- const baseFields = tableRules.length
- ? buildTargetFieldsFromRuleList(tableRules)
- : buildTargetFieldsFromTemplateInfo(template);
- return applyTableRuleRequiredFlags(baseFields, tableRules);
- };
- const getRequiredMatchStats = (
- fields: MappingTargetField[],
- mapping: Record<string, string>,
- recognitionRows: RecognitionBasicInfoRow[] = [],
- amountZfEnabled = false,
- ) => {
- const matchedByRecognitionBasicInfo = buildRecognitionBasicInfoMatchedTargetKeySet(recognitionRows, fields);
- const handledTargetKeySet = new Set(matchedByRecognitionBasicInfo);
- buildAmountZfHandledTargetKeySet(fields, amountZfEnabled).forEach((key) => {
- handledTargetKeySet.add(key);
- });
- const effectiveMapping = buildEffectiveFieldMapping(mapping, handledTargetKeySet);
- const requiredFields = fields.filter((item) => item.required);
- const requiredTotal = requiredFields.length;
- const requiredMatched = requiredFields.filter(
- (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
- ).length;
- const matchedCount = fields.filter(
- (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
- ).length;
- return { requiredTotal, requiredMatched, matchedCount };
- };
- const getSheetNodeVisualState = (item?: TreeFileInfo | null): SheetNodeVisualState => {
- if (!isTreeSheetNode(item)) return 'fail';
- const nodeKey = getTreeNodeKey(item);
- if (analyzingNodeKeys.value.includes(nodeKey)) {
- return 'analyzing';
- }
- const defaultState = getFileStatusVisualState(item.fileStatus) || (isSheetMatchComplete(item) ? 'success' : 'fail');
- const initialSignature = initialNodeStatusSignatureMap.get(nodeKey);
- if (activeNodeKey.value === nodeKey) {
- const currentState = buildNodeCacheState();
- const hasEditedState = !initialSignature || buildNodeStatusSignature(currentState) !== initialSignature;
- if (!hasEditedState) {
- return defaultState;
- }
- return isMatchSuccessful(requiredTotal.value, requiredMatchedCount.value, matchedFieldCount.value)
- ? 'success'
- : 'fail';
- }
- const cached = nodeStateCache.get(nodeKey);
- if (cached) {
- const hasEditedState = !initialSignature || buildNodeStatusSignature(cached) !== initialSignature;
- if (!hasEditedState) {
- return defaultState;
- }
- const {
- requiredTotal: cachedRequiredTotal,
- requiredMatched: cachedRequiredMatched,
- matchedCount: cachedMatchedCount,
- } = getRequiredMatchStats(
- resolveNodeTargetFields(item, cached.selectedTemplateId),
- cached.fieldMapping,
- cached.recognitionBasicInfoRows,
- cached.recognitionAmountZfEnabled,
- );
- return isMatchSuccessful(cachedRequiredTotal, cachedRequiredMatched, cachedMatchedCount) ? 'success' : 'fail';
- }
- return defaultState;
- };
- const mergeTemplateListItem = (template?: TemplateListItem | TableInfo | null) => {
- const templateId = normalizeTemplateId(template?.id);
- if (!template || !templateId) return;
- const nextItem = template as TemplateListItem;
- const targetIndex = templateList.value.findIndex((item) => normalizeTemplateId(item.id) === templateId);
- if (targetIndex >= 0) {
- templateList.value[targetIndex] = {
- ...templateList.value[targetIndex],
- ...nextItem,
- };
- return;
- }
- templateList.value = [...templateList.value, nextItem];
- };
- const syncActiveTemplateItem = (preferredId?: string | number | null) => {
- const preferredTemplateId = normalizeTemplateId(preferredId);
- const currentTemplateId = normalizeTemplateId(activeTemplateItem.value?.id);
- const currentBaseList = activeTemplateTab.value === 'plugin' ? pluginList.value : filteredTemplateList.value;
- if (preferredTemplateId) {
- const matched =
- currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) ||
- templateList.value.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) ||
- null;
- if (matched) {
- activeTemplateItem.value = matched;
- return matched;
- }
- }
- if (currentTemplateId) {
- const currentMatched =
- currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) ||
- templateList.value.find((item) => normalizeTemplateId(item.id) === currentTemplateId) ||
- null;
- if (currentMatched) {
- activeTemplateItem.value = currentMatched;
- return currentMatched;
- }
- }
- activeTemplateItem.value = null;
- return activeTemplateItem.value;
- };
- const syncModalTemplateItem = (preferredId?: string | number | null) => {
- const currentBaseList = filteredTemplateList.value;
- if (activeTemplateTab.value === 'plugin') {
- const currentPluginId = modalTemplateItem.value?.id;
- const matchedPlugin = currentBaseList.find((item) => String(item.id) === String(currentPluginId)) || null;
- modalTemplateItem.value = matchedPlugin;
- return modalTemplateItem.value;
- }
- const preferredTemplateId = normalizeTemplateId(preferredId);
- const currentTemplateId = normalizeTemplateId(modalTemplateItem.value?.id);
- if (preferredTemplateId) {
- const matched = currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) || null;
- if (matched) {
- modalTemplateItem.value = matched;
- return matched;
- }
- }
- if (currentTemplateId) {
- const currentMatched = currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) || null;
- if (currentMatched) {
- modalTemplateItem.value = currentMatched;
- return currentMatched;
- }
- }
- modalTemplateItem.value = currentBaseList[0] || null;
- return modalTemplateItem.value;
- };
- const mergeTemplateInfo = (
- base?: Partial<TemplateListItem> | Partial<TableInfo> | null,
- detail?: Partial<TableInfo> | null,
- ) => {
- if (!base && !detail) return null;
- return { ...(base || {}), ...(detail || {}) } as TemplateListItem & TableInfo;
- };
- const resolveTemplateInfoById = (templateId?: string | number | null) => {
- const id = normalizeTemplateId(templateId);
- if (!id) return null;
- const activeDetail = normalizeTemplateId(templateDetail.value?.id) === id ? templateDetail.value : null;
- const activeItem = normalizeTemplateId(activeTemplateItem.value?.id) === id ? activeTemplateItem.value : null;
- const cachedDetail = templateDetailCache.get(id) || null;
- const listItem = getTemplateListItemById(id) || mockTemplateMap[id] || null;
- return (
- mergeTemplateInfo(activeItem || listItem || activeDetail, activeDetail || cachedDetail) ||
- activeDetail ||
- cachedDetail ||
- activeItem ||
- listItem ||
- null
- );
- };
- const resolveParentTemplateInfoById = (templateId?: string | number | null) => {
- const template = resolveTemplateInfoById(templateId);
- const mainId = normalizeTemplateId((template as TableInfo | null)?.mainId);
- return (mainId && resolveTemplateInfoById(mainId)) || template;
- };
- const getSelectedTemplateMainId = (fallbackMainId?: string | number | null) => {
- if (templateDetail.value && Object.prototype.hasOwnProperty.call(templateDetail.value, 'mainId')) {
- return normalizeTemplateId(templateDetail.value.mainId);
- }
- if (activeTemplateItem.value && Object.prototype.hasOwnProperty.call(activeTemplateItem.value, 'mainId')) {
- return normalizeTemplateId(activeTemplateItem.value.mainId);
- }
- return normalizeTemplateId(fallbackMainId);
- };
- const syncModalTemplatePreviewState = (
- template?: TemplateListItem | null,
- options: { preferAppliedDetail?: boolean } = {},
- ) => {
- modalTemplateItem.value = template || null;
- modalTemplateDetail.value = null;
- templatePreviewLoading.value = false;
- templatePreviewRequestToken += 1;
- const id = normalizeTemplateId(template?.id);
- if (!id || Number(template?.id) < 0) return null;
- if (options.preferAppliedDetail && normalizeTemplateId(templateDetail.value?.id) === id && templateDetail.value) {
- modalTemplateDetail.value = templateDetail.value;
- return modalTemplateDetail.value;
- }
- const cachedDetail = templateDetailCache.get(id);
- if (cachedDetail) {
- modalTemplateDetail.value = cachedDetail;
- return modalTemplateDetail.value;
- }
- return null;
- };
- const loadTemplatePreviewDetail = async (
- template?: TemplateListItem | null,
- options: { preferAppliedDetail?: boolean } = {},
- ) => {
- const existingDetail = syncModalTemplatePreviewState(template, options);
- if (existingDetail) {
- return existingDetail;
- }
- const id = normalizeTemplateId(template?.id);
- if (!id || Number(template?.id) < 0) return null;
- const requestToken = ++templatePreviewRequestToken;
- templatePreviewLoading.value = true;
- try {
- const detail = await tplDetail({ id });
- if (requestToken !== templatePreviewRequestToken) return null;
- if (detail) {
- templateDetailCache.set(id, detail);
- modalTemplateDetail.value = detail;
- mergeTemplateListItem(mergeTemplateInfo(template, detail));
- modalTemplateItem.value = getTemplateListItemById(id) || template || null;
- }
- return modalTemplateDetail.value;
- } catch (error) {
- if (requestToken === templatePreviewRequestToken) {
- modalTemplateDetail.value = null;
- }
- return null;
- } finally {
- if (requestToken === templatePreviewRequestToken) {
- templatePreviewLoading.value = false;
- }
- }
- };
- const handleTemplateItemClick = async (template: TemplateListItem) => {
- if (activeTemplateTab.value === 'plugin' || Number(template.id) < 0) {
- syncModalTemplatePreviewState(template);
- return;
- }
- await loadTemplatePreviewDetail(template);
- };
- const handleTemplateItemConfirm = async (template: TemplateListItem) => {
- await handleTemplateItemClick(template);
- await confirmTemplateSelect();
- };
- const closeTemplateModal = () => {
- templateModalOpen.value = false;
- templatePreviewRequestToken += 1;
- templatePreviewLoading.value = false;
- modalTemplateDetail.value = null;
- modalTemplateItem.value = null;
- };
- const fetchAllTemplates = async () => {
- // 模板选择弹窗没有分页交互,打开时直接单次拉取较大的 limit,避免多次连续请求。
- const limit = 9999;
- const resp = await tplPage({ page: 1, limit } as any);
- const list = ((resp as any)?.list || (resp as any)?.records || []) as TemplateListItem[];
- const result = Array.isArray(list) ? list : [];
- const uniqueMap = new Map<string, TemplateListItem>();
- result.forEach((item, index) => {
- uniqueMap.set(String(item.id ?? `template_${index}`), item);
- });
- return Array.from(uniqueMap.values());
- };
- const loadTemplateList = async (options: { silent?: boolean; preferredId?: number } = {}) => {
- templateListLoading.value = true;
- try {
- const list = await fetchAllTemplates();
- templateList.value = mergeTemplateCollections(mockTemplateList, list);
- templateListLoaded.value = templateList.value.length > 0;
- if (!templateList.value.length && !options.silent) {
- createMessage.info('模板列表暂无可用数据');
- }
- } catch (error) {
- templateList.value = mergeTemplateCollections(mockTemplateList, templateList.value);
- templateListLoaded.value = templateList.value.length > 0;
- if (!options.silent && !templateList.value.length) {
- createMessage.warning('模板列表接口暂不可用');
- }
- } finally {
- templateListLoading.value = false;
- }
- syncActiveTemplateItem(options.preferredId ?? selectedTemplateId.value);
- if (selectedTemplateId.value) {
- syncSelectedTemplateState(selectedTemplateId.value);
- }
- };
- const syncSelectedTemplateState = (templateId?: string | number | null) => {
- const id = normalizeTemplateId(templateId ?? selectedTemplateId.value);
- selectedTemplateId.value = id;
- if (!id) {
- templateDetail.value = null;
- templateDetailFromMock.value = false;
- activeTemplateItem.value = null;
- return { id, matchedListItem: null, cachedDetail: null };
- }
- const matchedListItem = getTemplateListItemById(id);
- const cachedDetail = templateDetailCache.get(id) || null;
- templateDetail.value = cachedDetail
- ? mergeTemplateInfo(matchedListItem, cachedDetail)
- : matchedListItem || mockTemplateMap[id] || null;
- templateDetailFromMock.value = !cachedDetail && !matchedListItem && !!mockTemplateMap[id];
- if (templateDetail.value) {
- mergeTemplateListItem(templateDetail.value);
- }
- activeTemplateItem.value =
- getTemplateListItemById(id) || matchedListItem || (templateDetail.value as TemplateListItem | null) || null;
- return { id, matchedListItem, cachedDetail };
- };
- const loadTemplateDetail = async (
- templateId?: string | number | null,
- options: { silent?: boolean; requestDetail?: boolean } = {},
- ) => {
- const id = normalizeTemplateId(templateId ?? selectedTemplateId.value);
- if (!id) {
- syncSelectedTemplateState(id);
- return templateDetail.value;
- }
- if (!templateListLoaded.value && !templateListLoading.value) {
- await loadTemplateList({ silent: true, preferredId: id });
- }
- const { matchedListItem, cachedDetail } = syncSelectedTemplateState(id);
- if (!id || !options.requestDetail || cachedDetail) {
- return templateDetail.value;
- }
- try {
- const resp = await tplDetail({ id });
- const detail = resp || null;
- if (detail) {
- templateDetailCache.set(id, detail);
- }
- templateDetail.value = detail
- ? mergeTemplateInfo(matchedListItem, detail)
- : matchedListItem || mockTemplateMap[id] || null;
- templateDetailFromMock.value = !detail && !matchedListItem && !!mockTemplateMap[id];
- if (templateDetail.value) {
- mergeTemplateListItem(templateDetail.value);
- activeTemplateItem.value = getTemplateListItemById(id) || (templateDetail.value as TemplateListItem);
- }
- } catch (error) {
- templateDetail.value = matchedListItem || mockTemplateMap[id] || null;
- templateDetailFromMock.value = !matchedListItem && !!mockTemplateMap[id];
- activeTemplateItem.value = (matchedListItem || (templateDetail.value as TemplateListItem | null)) ?? null;
- if (!options.silent) {
- createMessage.warning('模板详情接口暂不可用,已使用当前列表匹配结果');
- }
- }
- return templateDetail.value;
- };
- const openTemplateModal = async () => {
- templateModalOpen.value = true;
- activeTemplateTab.value = 'template';
- activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
- templateKeyword.value = '';
- templateSource.value = 0;
- await Promise.all([
- loadTemplateCategories(),
- loadTemplateList({ silent: true, preferredId: selectedTemplateId.value }),
- ]);
- const previewTarget = syncModalTemplateItem(selectedTemplateId.value);
- await loadTemplatePreviewDetail(previewTarget, { preferAppliedDetail: true });
- };
- watch([activeCategory, templateSource, templateKeyword, activeTemplateTab], async () => {
- if (!templateModalOpen.value) return;
- if (!showTemplateTabSwitch.value) {
- activeTemplateTab.value = 'template';
- }
- const previewTarget = syncModalTemplateItem(
- activeTemplateTab.value === 'plugin' ? undefined : selectedTemplateId.value,
- );
- await loadTemplatePreviewDetail(previewTarget, {
- preferAppliedDetail:
- activeTemplateTab.value !== 'plugin' && normalizeTemplateId(previewTarget?.id) === selectedTemplateId.value,
- });
- });
- watch(activeTemplateTab, (tab) => {
- if (!templateModalOpen.value) return;
- if (tab === 'plugin') {
- modalTemplateDetail.value = null;
- templatePreviewLoading.value = false;
- templatePreviewRequestToken += 1;
- }
- });
- watch(categories, (nextCategories) => {
- if (!nextCategories.includes(activeCategory.value)) {
- activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
- }
- });
- const modalPreviewTemplate = computed(() => mergeTemplateInfo(modalTemplateItem.value, modalTemplateDetail.value));
- const previewRules = computed(() => {
- const fields = buildTargetFieldsFromTemplateInfo(modalPreviewTemplate.value);
- if (fields.length) {
- return fields.map((item) => ({
- targetField: item.label || '-',
- originalField: item.key || '-',
- required: item.required,
- }));
- }
- return getTemplateHeaderList(modalPreviewTemplate.value).map((item) => ({
- targetField: item,
- originalField: item,
- required: false,
- }));
- });
- const modalTemplateHeaderList = computed(() => getTemplateHeaderList(modalPreviewTemplate.value));
- const modalTemplateSourceLabel = computed(() => getTemplateSourceLabel(modalPreviewTemplate.value));
- const modalTemplateFieldCount = computed(() => previewRules.value.length);
- const activeNodeInfo = computed(() => {
- return activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
- });
- const activeNodeTemplateMatchedFail = computed(() => isTemplateMatchedFailStatus(activeNodeInfo.value?.fileStatus));
- watch(
- [
- selectedTemplateId,
- templateDetail,
- activeTemplateItem,
- fieldMapping,
- rightTableHeaders,
- addedColumns,
- selectRules,
- selectedRules,
- headerRowIndex,
- addedColumnConfig,
- splitColumnConfig,
- recognitionDirectionFields,
- recognitionBasicInfoBaseFuncRegx,
- recognitionBasicInfoManualVisible,
- recognitionBasicInfoRows,
- recognitionAmountZfEnabled,
- childTemplateName,
- ],
- () => {
- syncActiveNodeStructure();
- },
- { deep: true },
- );
- const hasTemplateSelected = computed(() => {
- const activeTemplateId = normalizeTemplateId(
- templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
- );
- if (activeTemplateId > 0) return true;
- if (activeNodeTemplateMatchedFail.value) return false;
- return (
- normalizeTemplateId(activeNodeInfo.value?.templateId) > 0 ||
- normalizeTableRuleList(activeNodeInfo.value?.tableRuleList).length > 0 ||
- !!normalizeRuleText(activeNodeInfo.value?.tableName)
- );
- });
- const activeSheetDisplayName = computed(() => {
- return activeNodeInfo.value?.sheetName || activeNodeInfo.value?.fileName || '当前工作表';
- });
- const currentTemplateName = computed(() => {
- return (
- templateDetail.value?.tableNameCn ||
- activeTemplateItem.value?.tableNameCn ||
- activeNodeInfo.value?.tableName ||
- '-'
- );
- });
- const currentTemplateDisplayName = computed(() => {
- if (!hasTemplateSelected.value) return '未选择模板';
- return currentTemplateName.value === '-' ? '已选择模板' : currentTemplateName.value;
- });
- const showRecognitionAmountZfPanel = computed(
- () => recognitionAmountZfEnabled.value || fixedTargetFields.value.some((item) => isTransAmountTargetField(item)),
- );
- const recognitionRuleTabs = computed(() => {
- const tabs: string[] = [];
- if (showRecognitionAmountZfPanel.value) {
- tabs.push('正负标识');
- }
- if (hasRecognitionLoanTab.value) {
- tabs.push('借贷标志');
- }
- if (hasRecognitionCommunicationTab.value) {
- tabs.push('通信类别');
- }
- if (showRecognitionBasicInfoPanel.value) {
- tabs.push('基本信息配置');
- }
- return Array.from(new Set(tabs));
- });
- const showRecognitionPanel = computed(() => recognitionRuleTabs.value.length > 0);
- const activeRecognitionRuleDesc = computed(() => {
- const templateName = currentTemplateName.value === '-' ? '当前模板' : currentTemplateName.value;
- const ruleLabel = activeRecognitionRule.value || recognitionRuleTabs.value[0] || '识别规则';
- return `围绕「${templateName}」预置 ${ruleLabel} 的识别配置,后续可结合导入样本继续沉淀成子模板。`;
- });
- watch(
- recognitionRuleTabs,
- (tabs) => {
- if (!tabs.length) {
- activeRecognitionRule.value = '';
- return;
- }
- if (!tabs.includes(activeRecognitionRule.value)) {
- activeRecognitionRule.value = tabs[0];
- }
- },
- { immediate: true },
- );
- watch(
- fixedTargetFields,
- (fields) => {
- if (activeMappingTargetKey.value && !fields.some((item) => item.key === activeMappingTargetKey.value)) {
- activeMappingTargetKey.value = '';
- }
- },
- { deep: true },
- );
- const confirmTemplateSelect = async () => {
- if (!modalTemplateItem.value?.id) return;
- if (Number(modalTemplateItem.value.id) < 0) {
- createMessage.info('模板插件暂未接入详情逻辑');
- closeTemplateModal();
- return;
- }
- const selectedId = normalizeTemplateId(modalTemplateItem.value.id);
- const previousSelectedId = normalizeTemplateId(selectedTemplateId.value);
- const shouldResetTemplateState = !!previousSelectedId && previousSelectedId !== selectedId;
- const sourcePreviewSnapshot = shouldResetTemplateState ? captureSourcePreviewSnapshot() : null;
- if (shouldResetTemplateState) {
- structureSyncSuspended.value = true;
- try {
- clearActiveNodeTemplateState();
- resetNodeWorkState();
- if (sourcePreviewSnapshot) {
- restorePreviewSnapshot(sourcePreviewSnapshot);
- } else {
- await restorePreviewFromNode(activeNodeInfo.value);
- }
- } finally {
- structureSyncSuspended.value = false;
- }
- }
- selectedTemplateId.value = selectedId;
- activeTemplateItem.value = modalTemplateItem.value;
- if (modalTemplateDetail.value && normalizeTemplateId(modalTemplateDetail.value.id) === selectedId) {
- templateDetailCache.set(selectedId, modalTemplateDetail.value);
- }
- closeTemplateModal();
- await loadTemplateDetail(selectedTemplateId.value, { silent: true, requestDetail: true });
- applyRecognitionDirectionState(activeNodeInfo.value, { preferTemplate: true });
- applyRecognitionBasicInfoState(activeNodeInfo.value, { preferTemplate: true });
- applyRecognitionAmountZfState(activeNodeInfo.value, { preferTemplate: true });
- autoMatchFields(false);
- saveCurrentNodeState();
- };
- const startResize = (type: 'left' | 'middle', event: MouseEvent) => {
- event.preventDefault();
- resizeState.value = {
- type,
- startX: event.clientX,
- startLeft: leftPanelWidth.value,
- startMiddle: middlePanelWidth.value,
- };
- document.body.classList.add('is-resizing-layout');
- document.body.style.cursor = 'col-resize';
- };
- const applyResize = () => {
- if (!resizeState.value.type || pendingClientX.value === null) return;
- const deltaX = pendingClientX.value - resizeState.value.startX;
- if (resizeState.value.type === 'left' && !leftCollapsed.value) {
- const next = resizeState.value.startLeft + deltaX;
- leftPanelWidth.value = Math.min(maxLeftWidth, Math.max(minLeftWidth, next));
- }
- if (resizeState.value.type === 'middle') {
- const next = resizeState.value.startMiddle + deltaX;
- middlePanelWidth.value = Math.min(maxMiddleWidth, Math.max(minMiddleWidth, next));
- }
- rafId.value = null;
- };
- const onResizeMouseMove = (event: MouseEvent) => {
- if (!resizeState.value.type) return;
- event.preventDefault();
- pendingClientX.value = event.clientX;
- if (rafId.value === null) {
- rafId.value = window.requestAnimationFrame(applyResize);
- }
- };
- const stopResize = () => {
- resizeState.value.type = '';
- resizeState.value.startX = 0;
- pendingClientX.value = null;
- if (rafId.value !== null) {
- window.cancelAnimationFrame(rafId.value);
- rafId.value = null;
- }
- document.body.classList.remove('is-resizing-layout');
- document.body.style.cursor = '';
- };
- const resetAddColumnForm = () => {
- addColumnDialogOpen.value = false;
- addColumnForm.value = { name: '', mode: 'ref', contentFields: [], customContent: '' };
- selectedSourceField.value = '';
- selectedContentIndex.value = -1;
- draggingContentIndex.value = null;
- dragOverContentIndex.value = null;
- };
- const resetSplitForm = () => {
- splitColumnDialogOpen.value = false;
- splitColumnForm.value = {
- source: '',
- targetName1: '',
- targetName2: '',
- delimiterType: 'length',
- delimiterValue: '',
- };
- };
- const openAddColumn = () => {
- addColumnMode.value = 'add';
- addColumnDialogOpen.value = true;
- addColumnForm.value = { name: '', mode: 'ref', contentFields: [], customContent: '' };
- selectedSourceField.value = availableSourceHeaders.value[0] || '';
- selectedContentIndex.value = -1;
- draggingContentIndex.value = null;
- dragOverContentIndex.value = null;
- };
- const openSplitColumn = () => {
- if (!availableSourceHeaders.value.length) {
- createMessage.warning('当前没有可供拆分的原始列');
- return;
- }
- splitColumnDialogOpen.value = true;
- splitColumnForm.value = {
- source: availableSourceHeaders.value[0] || '',
- targetName1: '',
- targetName2: '',
- delimiterType: 'length',
- delimiterValue: '',
- };
- };
- const openEditColumn = () => {
- if (!selectedAddedColumn.value) {
- createMessage.warning('请先选中一个新增列');
- return;
- }
- addColumnMode.value = 'edit';
- addColumnDialogOpen.value = true;
- const config = addedColumnConfig.value[selectedAddedColumn.value];
- addColumnForm.value = {
- name: selectedAddedColumn.value,
- mode: config?.mode || 'ref',
- contentFields: (config?.contentFields || []).filter((field) => availableSourceHeaders.value.includes(field)),
- customContent: config?.customContent || '',
- };
- selectedSourceField.value = availableSourceHeaders.value[0] || '';
- selectedContentIndex.value = -1;
- draggingContentIndex.value = null;
- dragOverContentIndex.value = null;
- };
- const openDeleteColumn = () => {
- if (!selectedAddedColumn.value) {
- createMessage.warning('请先选中一个新增列');
- return;
- }
- deleteColumnDialogOpen.value = true;
- };
- const appendSourceField = (field: string) => {
- if (addColumnForm.value.mode !== 'ref') return;
- if (!availableSourceHeaders.value.includes(field)) return;
- addColumnForm.value.contentFields.push(field);
- selectedContentIndex.value = addColumnForm.value.contentFields.length - 1;
- };
- const removeContentField = (index: number) => {
- addColumnForm.value.contentFields.splice(index, 1);
- if (selectedContentIndex.value >= addColumnForm.value.contentFields.length) {
- selectedContentIndex.value = addColumnForm.value.contentFields.length - 1;
- }
- if (draggingContentIndex.value !== null && draggingContentIndex.value >= addColumnForm.value.contentFields.length) {
- draggingContentIndex.value = null;
- }
- if (dragOverContentIndex.value !== null && dragOverContentIndex.value >= addColumnForm.value.contentFields.length) {
- dragOverContentIndex.value = null;
- }
- };
- const clearContentDragState = () => {
- draggingContentIndex.value = null;
- dragOverContentIndex.value = null;
- };
- const reorderContentFields = (fromIndex: number, insertIndex: number) => {
- if (fromIndex < 0 || fromIndex >= addColumnForm.value.contentFields.length) return;
- const list = [...addColumnForm.value.contentFields];
- const [field] = list.splice(fromIndex, 1);
- if (field === undefined) return;
- const nextIndex = Math.min(Math.max(insertIndex - (fromIndex < insertIndex ? 1 : 0), 0), list.length);
- list.splice(nextIndex, 0, field);
- addColumnForm.value.contentFields = list;
- selectedContentIndex.value = nextIndex;
- draggingContentIndex.value = nextIndex;
- dragOverContentIndex.value = nextIndex;
- };
- const resolveContentDropInsertIndex = (index: number, event: DragEvent) => {
- const currentTarget = event.currentTarget as HTMLElement | null;
- if (!currentTarget) return index;
- const rect = currentTarget.getBoundingClientRect();
- return event.clientY > rect.top + rect.height / 2 ? index + 1 : index;
- };
- const handleContentDragStart = (index: number, event: DragEvent) => {
- draggingContentIndex.value = index;
- dragOverContentIndex.value = index;
- selectedContentIndex.value = index;
- if (event.dataTransfer) {
- event.dataTransfer.effectAllowed = 'move';
- event.dataTransfer.setData('text/plain', String(index));
- }
- };
- const handleContentDragEnter = (index: number) => {
- if (draggingContentIndex.value === null) return;
- dragOverContentIndex.value = index;
- };
- const handleContentDragOver = (index: number, event: DragEvent) => {
- if (draggingContentIndex.value === null) return;
- event.preventDefault();
- dragOverContentIndex.value = index;
- if (event.dataTransfer) {
- event.dataTransfer.dropEffect = 'move';
- }
- };
- const handleContentDrop = (index: number, event: DragEvent) => {
- if (draggingContentIndex.value === null) return;
- event.preventDefault();
- const insertIndex = resolveContentDropInsertIndex(index, event);
- reorderContentFields(draggingContentIndex.value, insertIndex);
- clearContentDragState();
- };
- const handleContentDragEnd = () => {
- clearContentDragState();
- };
- const getEnabledGroupingColumnsForHeader = (header: string) => {
- if (!isCleaningRuleEnabledInUi('groupingColumns')) {
- return [];
- }
- const columnIndex = rightTableHeaders.value.findIndex((item) => item === header);
- if (columnIndex < 0) return [];
- const groupingRule = [...(selectRules.value[columnIndex] || [])]
- .reverse()
- .find((rule) => rule.enabled && rule.key === 'groupingColumns');
- if (!groupingRule) return [];
- return normalizeGroupingColumnsDraft(groupingRule.rules?.groupColumns).filter(
- (item) => !!item.header && !!item.direction && Number(item.count ?? 0) > 0,
- );
- };
- const buildEnabledGroupingColumnsPayload = () =>
- rightTableHeaders.value.reduce(
- (acc, header) => {
- if (!addedColumns.value.includes(header)) {
- return acc;
- }
- const groupColumns = getEnabledGroupingColumnsForHeader(header);
- if (!groupColumns.length) {
- return acc;
- }
- acc.push({
- targetHeader: header,
- groupFlag: true,
- groupColumns: cloneDeepSafe(groupColumns),
- });
- return acc;
- },
- [] as Array<{
- targetHeader: string;
- groupFlag: boolean;
- groupColumns: Array<Record<string, any>>;
- }>,
- );
- const setGeneratedColumnCell = (row: Record<string, string>, rowIndex: number, columnName: string, value: string) => {
- row[columnName] = getGeneratedColumnHeaderRowIndex(columnName) === rowIndex ? columnName : value;
- };
- const rebuildGeneratedColumnsInSourceRows = () => {
- if (!addedColumns.value.length || !sourceTableRows.value.length) return;
- const addedHeaderSet = new Set(addedColumns.value);
- sourceTableRows.value = sourceTableRows.value.map((row, rowIndex) => {
- const nextRow = { ...row };
- const handledSplitColumns = new Set<string>();
- rightTableHeaders.value.forEach((header) => {
- if (!addedHeaderSet.has(header)) return;
- const groupingColumns = getEnabledGroupingColumnsForHeader(header);
- if (groupingColumns.length) {
- setGeneratedColumnCell(
- nextRow,
- rowIndex,
- header,
- String(groupColumnsFnValue(nextRow, groupingColumns) ?? ''),
- );
- return;
- }
- const splitConfig = splitColumnConfig.value[header];
- if (splitConfig) {
- const splitKey = [
- splitConfig.source,
- splitConfig.targetName1,
- splitConfig.targetName2,
- splitConfig.delimiterType,
- splitConfig.delimiterValue,
- ].join('::');
- if (handledSplitColumns.has(splitKey)) return;
- handledSplitColumns.add(splitKey);
- const sourceValue = String(nextRow[splitConfig.source] ?? '');
- const splitResult = splitSourceValueByConfig(
- sourceValue,
- splitConfig.delimiterType,
- splitConfig.delimiterValue,
- );
- if (splitConfig.targetName1 && addedHeaderSet.has(splitConfig.targetName1)) {
- setGeneratedColumnCell(nextRow, rowIndex, splitConfig.targetName1, splitResult.first);
- }
- if (splitConfig.targetName2 && addedHeaderSet.has(splitConfig.targetName2)) {
- setGeneratedColumnCell(nextRow, rowIndex, splitConfig.targetName2, splitResult.second);
- }
- return;
- }
- const addedConfig = addedColumnConfig.value[header];
- if (!addedConfig) return;
- const nextValue =
- addedConfig.mode === 'custom'
- ? addedConfig.customContent || ''
- : addedConfig.contentFields.map((field) => String(nextRow[field] ?? '')).join('');
- setGeneratedColumnCell(nextRow, rowIndex, header, nextValue);
- });
- return nextRow;
- });
- };
- const confirmAddOrEditColumn = () => {
- const name = addColumnForm.value.name.trim();
- const normalizedName = normalizeColumnNameForCompare(name);
- const validContentFields = addColumnForm.value.contentFields.filter((field) =>
- availableSourceHeaders.value.includes(field),
- );
- if (!name) {
- createMessage.warning(addColumnMode.value === 'add' ? '请输入新增列名' : '请输入编辑列名');
- return;
- }
- if (addColumnForm.value.mode === 'ref' && !validContentFields.length) {
- createMessage.warning('请至少选择一个原始数据列');
- return;
- }
- if (addColumnForm.value.mode === 'custom' && !addColumnForm.value.customContent.trim()) {
- createMessage.warning('请输入自定义列内容');
- return;
- }
- const saveConfig = {
- mode: addColumnForm.value.mode,
- contentFields: [...validContentFields],
- customContent: addColumnForm.value.customContent,
- };
- if (addColumnMode.value === 'add') {
- if (hasDuplicateHeaderName(name)) {
- createMessage.warning('列名已存在');
- return;
- }
- rightTableHeaders.value.push(name);
- addedColumns.value.push(name);
- addedColumnConfig.value[name] = saveConfig;
- generatedColumnHeaderRowMap.value[name] = headerRowIndex.value;
- sourceTableRows.value.forEach((row, rowIndex) => {
- if (saveConfig.mode === 'custom') {
- setGeneratedColumnCell(row, rowIndex, name, saveConfig.customContent || '');
- } else {
- setGeneratedColumnCell(
- row,
- rowIndex,
- name,
- saveConfig.contentFields.map((field) => row[field] || '').join(''),
- );
- }
- });
- selectedAddedColumn.value = name;
- currentColumn.value = rightTableHeaders.value.findIndex((h) => h === name);
- createMessage.success('新增列成功');
- } else {
- const oldName = selectedAddedColumn.value;
- if (!oldName) return;
- const preservedHeaderRowIndex = getGeneratedColumnHeaderRowIndex(oldName);
- const normalizedOldName = normalizeColumnNameForCompare(oldName);
- if (normalizedName !== normalizedOldName && hasDuplicateHeaderName(name, [oldName])) {
- createMessage.warning('列名已存在');
- return;
- }
- rightTableHeaders.value = rightTableHeaders.value.map((h) => (h === oldName ? name : h));
- addedColumns.value = addedColumns.value.map((h) => (h === oldName ? name : h));
- Object.keys(fieldMapping.value).forEach((key) => {
- if (fieldMapping.value[key] === oldName) {
- fieldMapping.value[key] = name;
- }
- });
- const headerKeyMap = oldName && name !== oldName ? { [oldName]: name } : {};
- if (Object.keys(headerKeyMap).length) {
- selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, headerKeyMap);
- groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, headerKeyMap);
- }
- if (name !== oldName) {
- delete generatedColumnHeaderRowMap.value[oldName];
- }
- generatedColumnHeaderRowMap.value[name] = preservedHeaderRowIndex;
- sourceTableRows.value.forEach((row, rowIndex) => {
- if (saveConfig.mode === 'custom') {
- setGeneratedColumnCell(row, rowIndex, name, saveConfig.customContent || '');
- } else {
- setGeneratedColumnCell(
- row,
- rowIndex,
- name,
- saveConfig.contentFields.map((field) => row[field] || '').join(''),
- );
- }
- if (name !== oldName) {
- delete row[oldName];
- }
- });
- delete addedColumnConfig.value[oldName];
- addedColumnConfig.value[name] = saveConfig;
- selectedAddedColumn.value = name;
- currentColumn.value = rightTableHeaders.value.findIndex((h) => h === name);
- createMessage.success('编辑列成功');
- }
- runRulePipeline();
- resetAddColumnForm();
- };
- const confirmSplitColumn = () => {
- const source = splitColumnForm.value.source;
- const target1 = splitColumnForm.value.targetName1.trim();
- const target2 = splitColumnForm.value.targetName2.trim();
- const normalizedTarget1 = normalizeColumnNameForCompare(target1);
- const normalizedTarget2 = normalizeColumnNameForCompare(target2);
- const delimiterType = normalizeSplitColumnMode(splitColumnForm.value.delimiterType);
- const delimiterValue = normalizeSplitColumnValue(
- splitColumnForm.value.delimiterType,
- splitColumnForm.value.delimiterValue,
- );
- const splitLength = resolveSplitColumnLength(
- splitColumnForm.value.delimiterType,
- splitColumnForm.value.delimiterValue,
- );
- if (!source) {
- createMessage.warning('请选择需要拆分的列');
- return;
- }
- if (!availableSourceHeaders.value.includes(source)) {
- createMessage.warning('拆分来源只能选择原始列');
- return;
- }
- if (!target1) {
- createMessage.warning('请输入拆分后列名1');
- return;
- }
- if (!target2) {
- createMessage.warning('请输入拆分后列名2');
- return;
- }
- if (normalizedTarget1 === normalizedTarget2) {
- createMessage.warning('拆分后两个列名不能相同');
- return;
- }
- if (hasDuplicateHeaderName(target1)) {
- createMessage.warning('拆分后列名1已存在');
- return;
- }
- if (hasDuplicateHeaderName(target2)) {
- createMessage.warning('拆分后列名2已存在');
- return;
- }
- if (delimiterType === 'length' && !splitLength) {
- createMessage.warning('请输入正确的字符长度');
- return;
- }
- if (delimiterType === 'special' && !delimiterValue) {
- createMessage.warning('请输入特殊标识');
- return;
- }
- // 拆分后新列统一追加到最右侧,保持新增列区域集中展示。
- rightTableHeaders.value.push(target1, target2);
- addedColumns.value.push(target1, target2);
- generatedColumnHeaderRowMap.value[target1] = headerRowIndex.value;
- generatedColumnHeaderRowMap.value[target2] = headerRowIndex.value;
- sourceTableRows.value.forEach((row, rowIndex) => {
- const sourceVal = String(row[source] || '');
- const splitResult = splitSourceValueByConfig(sourceVal, delimiterType, delimiterValue);
- setGeneratedColumnCell(row, rowIndex, target1, splitResult.first);
- setGeneratedColumnCell(row, rowIndex, target2, splitResult.second);
- });
- // 记录拆分配置
- const config = {
- source,
- delimiterType,
- delimiterValue,
- targetName1: target1,
- targetName2: target2,
- };
- splitColumnConfig.value[target1] = config;
- splitColumnConfig.value[target2] = config;
- selectedAddedColumn.value = target1;
- currentColumn.value = rightTableHeaders.value.findIndex((h) => h === target1);
- runRulePipeline();
- createMessage.success('拆分列成功');
- resetSplitForm();
- };
- const confirmDeleteColumn = () => {
- const name = selectedAddedColumn.value;
- if (!name) return;
- rightTableHeaders.value = rightTableHeaders.value.filter((h) => h !== name);
- addedColumns.value = addedColumns.value.filter((h) => h !== name);
- delete addedColumnConfig.value[name];
- const deletedSplitConfig = splitColumnConfig.value[name];
- delete splitColumnConfig.value[name];
- if (deletedSplitConfig) {
- [deletedSplitConfig.targetName1, deletedSplitConfig.targetName2].filter(Boolean).forEach((targetName) => {
- const targetConfig = splitColumnConfig.value[targetName];
- if (targetName === name || !targetConfig) return;
- splitColumnConfig.value[targetName] = {
- ...targetConfig,
- targetName1: targetConfig.targetName1 === name ? '' : targetConfig.targetName1,
- targetName2: targetConfig.targetName2 === name ? '' : targetConfig.targetName2,
- };
- });
- }
- delete generatedColumnHeaderRowMap.value[name];
- sourceTableRows.value.forEach((row) => {
- delete row[name];
- });
- Object.keys(fieldMapping.value).forEach((key) => {
- if (fieldMapping.value[key] === name) {
- fieldMapping.value[key] = '';
- }
- });
- selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, {}, new Set([name]));
- groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, {}, new Set([name]));
- selectedAddedColumn.value = addedColumns.value[0] || '';
- currentColumn.value = selectedAddedColumn.value
- ? rightTableHeaders.value.findIndex((h) => h === selectedAddedColumn.value)
- : null;
- selectedRowIndex.value = null;
- runRulePipeline();
- deleteColumnDialogOpen.value = false;
- createMessage.success('删除列成功');
- };
- const onClickHeader = (header: string) => {
- const index = rightTableHeaders.value.findIndex((h) => h === header);
- if (index < 0) return;
- closeMappingPopoverPreview();
- focusPreviewColumn(header, index, { resetSelectedRow: true });
- };
- const onClickCell = (rowIndex: number, header: string, colIndex: number) => {
- closeMappingPopoverPreview();
- selectedRowIndex.value = rowIndex;
- focusPreviewColumn(header, colIndex, { resetSelectedRow: false });
- };
- const runRulePipeline = () => {
- rebuildGeneratedColumnsInSourceRows();
- const previewRows = runCleaningPreviewPipeline({
- rows: sourceTableRows.value,
- headers: rightTableHeaders.value,
- columnRules: selectRules.value.map((rules) =>
- rules.filter((rule) => rule.enabled && isCleaningRuleEnabledInUi(rule.key)),
- ),
- ignoredRows: ignoredRowsSet.value,
- headerRowIndex: headerRowIndex.value,
- });
- tablePreviewRows.value = normalizePreviewRowsByTargetFormat(previewRows);
- };
- const rulesListCheck = (index: number) => {
- activeRules.value = index;
- cleaningRulePanelExpanded.value = true;
- currentRuleTitle.value = '';
- cleaningRulesList.value = cleaningRulesList.value.map((item, i) => {
- if (index === 0) {
- return { ...item, show: i !== 0, active: false };
- }
- return { ...item, show: i === 0, active: false };
- });
- };
- const rulesListClick = (key: CleaningRuleKey) => {
- cleaningRulePanelExpanded.value = true;
- cleaningRulesList.value = cleaningRulesList.value.map((item) => ({
- ...item,
- active: item.key === key,
- }));
- currentRuleTitle.value = cleaningRulesList.value.find((item) => item.key === key)?.name || '';
- if (isSharedRuleMode.value) {
- selectedRules.value = selectedRules.value.map((item) => ({ ...item, active: false }));
- return;
- }
- if (currentColumn.value !== null) {
- const list = selectRules.value[currentColumn.value] || [];
- selectRules.value[currentColumn.value] = list.map((item) => ({ ...item, active: false }));
- }
- };
- const rulesListRight = () => {
- const activeRule = cleaningRulesList.value.find((item) => item.active);
- if (!activeRule) {
- createMessage.warning('请先选择一个清洗规则');
- return;
- }
- if (isSharedRuleMode.value && activeRule.key === 'ignoreLine') {
- selectedRules.value = [
- {
- name: activeRule.name,
- key: activeRule.key,
- active: true,
- enabled: false,
- rules: defaultRuleConfig(),
- },
- ];
- omitLineInput.value = String(selectedRules.value[0].rules.omitLine || '');
- currentRuleTitle.value = activeRule.name;
- return;
- }
- if (currentColumn.value === null) {
- createMessage.warning('请先选中一列');
- return;
- }
- if (
- activeRule.key === 'groupingColumns' &&
- !addedColumns.value.includes(rightTableHeaders.value[currentColumn.value])
- ) {
- createMessage.warning('组合列只能配置在新增列上');
- return;
- }
- if (!selectRules.value[currentColumn.value]) {
- selectRules.value[currentColumn.value] = [];
- }
- selectRules.value[currentColumn.value].push({
- name: activeRule.name,
- key: activeRule.key,
- active: false,
- enabled: false,
- rules: defaultRuleConfig(),
- });
- const lastIndex = selectRules.value[currentColumn.value].length - 1;
- selectRulesClick(lastIndex);
- runRulePipeline();
- };
- const dbRulesListClick = (key: CleaningRuleKey) => {
- rulesListClick(key);
- rulesListRight();
- };
- const hydrateRuleFormByActiveRule = (rule?: SelectedRuleItem) => {
- if (!rule) return;
- if (rule.key === 'ignoreLine') {
- omitLineInput.value = String(rule.rules?.omitLine || '');
- }
- if (rule.key === 'dateConfig') {
- dateConfigType.value = Number(rule.rules?.dateType ?? 1) || 1;
- }
- if (rule.key === 'extractData') {
- extractDataBefore.value = String(rule.rules?.beforeModel || '');
- extractDataAfter.value = String(rule.rules?.nextModel || '');
- syncExtractDataMode();
- }
- if (rule.key === 'extractSegment') {
- extractSegmentValue.value = String(rule.rules?.segment || '');
- extractSegmentRow.value = Number(rule.rules?.segmentRow ?? 1) || 1;
- }
- if (rule.key === 'absoluteValue') {
- absoluteValueChecked.value = toRuleBoolean(rule.rules?.absoluteData, false);
- }
- if (rule.key === 'decimalConversion') {
- decimalEnabled.value = toRuleBoolean(rule.rules?.decimalData, false);
- decimalSourceBase.value = normalizeDecimalBaseLabel(rule.rules?.decimalModel2 || '10进制') || '10进制';
- decimalTargetBase.value = normalizeDecimalBaseLabel(rule.rules?.decimalModel || '16进制') || '16进制';
- }
- if (rule.key === 'replaceValue') {
- replaceEnabled.value = toRuleBoolean(rule.rules?.replaceFlag, true);
- replaceMode.value = Number(rule.rules?.replaceModel ?? 0) === 1 ? 1 : 0;
- replaceSource.value = String(rule.rules?.sourceData || '');
- replaceTarget.value = String(rule.rules?.targetData || '');
- }
- if (rule.key === 'removeSpecialCharacters') {
- const specialList: string[] = rule.rules?.specialList || [];
- removeSpaceChecked.value = specialList.includes('移除空格字符');
- removeTabChecked.value = specialList.includes('移除换行字符(tab)');
- removeCustomChecked.value = specialList.includes('要移除的特殊字符');
- removeCustomText.value = String(rule.rules?.specialStr || '');
- }
- if (rule.key === 'groupingColumns') {
- groupingEnabled.value = toRuleBoolean(rule.rules?.groupFlag, false);
- groupingColumnsDraft.value = normalizeGroupingColumnsDraft(rule.rules?.groupColumns);
- }
- if (rule.key === 'magnificationAdjustment') {
- magnificationValue.value = Number(rule.rules?.magnificationAdjustmentValue ?? 1) || 1;
- }
- };
- const selectRulesClick = (index: number) => {
- if (isSharedRuleMode.value) {
- selectedRules.value = selectedRules.value.map((item, idx) => ({
- ...item,
- active: idx === index,
- }));
- const activeRule = selectedRules.value[index];
- currentRuleTitle.value = activeRule?.name || '';
- hydrateRuleFormByActiveRule(activeRule);
- return;
- }
- if (currentColumn.value === null) return;
- const list = selectRules.value[currentColumn.value] || [];
- selectRules.value[currentColumn.value] = list.map((item, idx) => ({
- ...item,
- active: idx === index,
- }));
- const activeRule = selectRules.value[currentColumn.value][index];
- currentRuleTitle.value = activeRule?.name || '';
- hydrateRuleFormByActiveRule(activeRule);
- };
- const rulesListLeft = () => {
- if (isSharedRuleMode.value) {
- const activeIndex = selectedRules.value.findIndex((item) => item.active);
- if (activeIndex < 0) {
- createMessage.warning('请先选择一个已选规则');
- return;
- }
- selectedRules.value.splice(activeIndex, 1);
- selectedRules.value = [...selectedRules.value];
- omitLineInput.value = '';
- currentRuleTitle.value = '';
- runRulePipeline();
- return;
- }
- if (currentColumn.value === null) {
- createMessage.warning('请先选中一列');
- return;
- }
- const list = selectRules.value[currentColumn.value] || [];
- const activeIndex = list.findIndex((item) => item.active);
- if (activeIndex < 0) {
- createMessage.warning('请先选择一个已选规则');
- return;
- }
- list.splice(activeIndex, 1);
- selectRules.value[currentColumn.value] = [...list];
- currentRuleTitle.value = '';
- runRulePipeline();
- };
- const selectRulesDel = () => {
- if (isSharedRuleMode.value) {
- selectedRules.value = [];
- omitLineInput.value = '';
- currentRuleTitle.value = '';
- runRulePipeline();
- return;
- }
- if (currentColumn.value === null) {
- createMessage.warning('请先选中一列');
- return;
- }
- selectRules.value[currentColumn.value] = [];
- currentRuleTitle.value = '';
- runRulePipeline();
- };
- const getCurrentActiveRule = (): SelectedRuleItem | undefined => {
- if (isSharedRuleMode.value) {
- return selectedRules.value.find((rule) => rule.active);
- }
- if (currentColumn.value === null) return undefined;
- return (selectRules.value[currentColumn.value] || []).find((rule) => rule.active);
- };
- const refreshActiveRuleCollection = () => {
- if (isSharedRuleMode.value) {
- selectedRules.value = [...selectedRules.value];
- return;
- }
- selectRules.value = [...selectRules.value];
- };
- const updateCurrentRuleConfig = (
- expectedKey: CleaningRuleKey,
- warningText: string,
- successText: string,
- updater: (rule: SelectedRuleItem) => void,
- ) => {
- const activeRule = getCurrentActiveRule();
- if (!activeRule || activeRule.key !== expectedKey) {
- createMessage.warning(warningText);
- return false;
- }
- updater(activeRule);
- activeRule.enabled = true;
- refreshActiveRuleCollection();
- runRulePipeline();
- createMessage.success(successText);
- return true;
- };
- const toggleCurrentRuleEnabled = (enabled: boolean) => {
- const activeRule = getCurrentActiveRule();
- if (!activeRule) {
- createMessage.warning('请先选择一个已选规则');
- return;
- }
- if (activeRule.enabled === enabled) {
- return;
- }
- activeRule.enabled = enabled;
- refreshActiveRuleCollection();
- runRulePipeline();
- createMessage.success(enabled ? '规则已启用' : '规则已停用');
- };
- const getFilledGroupingFieldCount = (item: GroupingColumnDraft) =>
- [!!item.header, !!item.direction, item.count !== undefined && item.count > 0].filter(Boolean).length;
- const addGroupingColumnDraft = () => {
- groupingColumnsDraft.value = [...groupingColumnsDraft.value, createGroupingColumnDraft()];
- };
- const removeGroupingColumnDraft = (index: number) => {
- const next = groupingColumnsDraft.value.filter((_, itemIndex) => itemIndex !== index);
- groupingColumnsDraft.value = next.length ? next : normalizeGroupingColumnsDraft();
- };
- const handleGroupingColumnHeaderChange = (index: number, value: string) => {
- const row = groupingColumnsDraft.value[index];
- if (!row) return;
- row.header = value;
- row.title = value;
- groupingColumnsDraft.value = [...groupingColumnsDraft.value];
- };
- const applyIgnoreLineRule = () => {
- updateCurrentRuleConfig('ignoreLine', '请先选择忽略行规则', '忽略行规则已应用', (rule) => {
- rule.rules.omitLine = omitLineInput.value || '';
- });
- };
- const applyDateConfigRule = () => {
- updateCurrentRuleConfig('dateConfig', '请先选择日期和时间规则', '日期和时间规则已应用', (rule) => {
- rule.rules.dateType = dateConfigType.value;
- });
- };
- const applyExtractDataRule = () => {
- updateCurrentRuleConfig('extractData', '请先选择提取数据规则', '提取数据规则已应用', (rule) => {
- rule.rules.beforeModel = extractDataMode.value === 'before' ? extractDataBefore.value || '' : '';
- rule.rules.nextModel = extractDataMode.value === 'after' ? extractDataAfter.value || '' : '';
- });
- };
- const applyExtractSegmentRule = () => {
- if (!extractSegmentValue.value.trim()) {
- createMessage.warning('请输入分隔符');
- return;
- }
- updateCurrentRuleConfig('extractSegment', '请先选择分割提取规则', '分割提取规则已应用', (rule) => {
- rule.rules.segment = extractSegmentValue.value || '';
- rule.rules.segmentRow = Number(extractSegmentRow.value ?? 1) || 1;
- });
- };
- const applyTimestampConversionRule = () => {
- updateCurrentRuleConfig('timestampConversion', '请先选择时间戳转换规则', '时间戳转换规则已启用', (rule) => {
- rule.rules.timeStamp = true;
- });
- };
- const applyAbsoluteValueRule = () => {
- updateCurrentRuleConfig('absoluteValue', '请先选择绝对值规则', '绝对值规则已应用', (rule) => {
- rule.rules.absoluteData = absoluteValueChecked.value;
- });
- };
- const applyDecimalConversionRule = () => {
- updateCurrentRuleConfig('decimalConversion', '请先选择进制转换规则', '进制转换规则已应用', (rule) => {
- rule.rules.decimalData = decimalEnabled.value;
- rule.rules.decimalModel = decimalTargetBase.value;
- rule.rules.decimalModel2 = decimalSourceBase.value;
- });
- };
- const applyReplaceRule = () => {
- updateCurrentRuleConfig('replaceValue', '请先选择条件替换规则', '条件替换规则已应用', (rule) => {
- rule.rules.replaceFlag = replaceEnabled.value;
- rule.rules.replaceModel = replaceMode.value;
- rule.rules.sourceData = replaceSource.value || '';
- rule.rules.targetData = replaceTarget.value || '';
- });
- };
- const applyRemoveSpecialRule = () => {
- const specialList: string[] = [];
- if (removeSpaceChecked.value) specialList.push('移除空格字符');
- if (removeTabChecked.value) specialList.push('移除换行字符(tab)');
- if (removeCustomChecked.value) specialList.push('要移除的特殊字符');
- updateCurrentRuleConfig('removeSpecialCharacters', '请先选择移除特殊字符规则', '移除特殊字符规则已应用', (rule) => {
- rule.rules.specialList = specialList;
- rule.rules.specialStr = removeCustomText.value || '';
- });
- };
- const applyGroupingColumnsRule = () => {
- const normalizedRows = groupingColumnsDraft.value.map((item) => createGroupingColumnDraft(item));
- const hasInvalidRow = normalizedRows.some((item) => {
- const filledCount = getFilledGroupingFieldCount(item);
- return filledCount > 0 && filledCount < 3;
- });
- if (hasInvalidRow) {
- createMessage.warning('组合列每一行都需要完整选择表头、方向和位数');
- return;
- }
- const hasValidRow = normalizedRows.some((item) => getFilledGroupingFieldCount(item) === 3);
- if (!hasValidRow) {
- createMessage.warning('请至少配置一条完整的组合列规则');
- return;
- }
- updateCurrentRuleConfig('groupingColumns', '请先选择组合列规则', '组合列规则已应用', (rule) => {
- rule.rules.groupFlag = true;
- rule.rules.groupColumns = normalizedRows;
- });
- };
- const applyMagnificationRule = () => {
- updateCurrentRuleConfig('magnificationAdjustment', '请先选择倍数放大器规则', '倍数放大器规则已应用', (rule) => {
- rule.rules.magnificationAdjustmentValue = Number(magnificationValue.value ?? 1) || 1;
- });
- };
- const applyYearCompleteRule = () => {
- updateCurrentRuleConfig('yearComplete', '请先选择年份补全规则', '年份补全规则已启用', (rule) => {
- rule.rules.yearCompleteEnabled = true;
- });
- };
- const toggleLeft = () => {
- leftCollapsed.value = !leftCollapsed.value;
- };
- const handleSelect = (keys: (string | number)[]) => {
- const rawKey = keys[0] ? String(keys[0]) : '';
- const nextKey = rawKey ? resolveSelectableTreeKey(rawKey) : '';
- selectedKeys.value = nextKey ? [nextKey] : [];
- if (nextKey) {
- switchNodeState(nextKey);
- }
- };
- const handleImportData = async () => {
- saveCurrentNodeState();
- syncActiveNodeStructure();
- const sanitizedFileInfos = sanitizeFileInfoTree(fileList.value, { keepRuleKey: false });
- if (!sanitizedFileInfos.length) {
- createMessage.warning('未找到可导入的数据,请先上传并完成模板匹配');
- return;
- }
- const matchedSheetsWithoutHeaderRow = collectMatchedSheetsWithoutHeaderRow(sanitizedFileInfos);
- if (matchedSheetsWithoutHeaderRow.length) {
- const previewNames = matchedSheetsWithoutHeaderRow.slice(0, 3).join('、');
- const suffix =
- matchedSheetsWithoutHeaderRow.length > 3 ? ` 等 ${matchedSheetsWithoutHeaderRow.length} 个工作表` : '';
- createMessage.warning(`已匹配模板的工作表必须先填写表头所在行:${previewNames}${suffix}`);
- return;
- }
- try {
- const childTemplateNameValid = await ensureChildTemplateNamesAvailable(sanitizedFileInfos);
- if (!childTemplateNameValid) {
- return;
- }
- } catch (error) {
- createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
- return;
- }
- sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizedFileInfos));
- importingData.value = true;
- try {
- const resolveMappedHeaderByTargetField = (item?: TreeFileInfo | null, targetField?: string) => {
- const normalizedTarget = normalizeFieldMatchText(targetField);
- if (!normalizedTarget) return '';
- const matchedRule = normalizeTableRuleList(item?.tableRuleList).find((rule, index) => {
- const targetKey = normalizeFieldMatchText(getTableRuleTargetKey(rule, index));
- const targetLabel = normalizeFieldMatchText(getTableRuleTargetLabel(rule, index));
- return normalizedTarget === targetKey || normalizedTarget === targetLabel;
- });
- return normalizeRecognitionConfigText(matchedRule?.fileColName);
- };
- const buildImportRecognitionBasicInfoFuncRegx = (item?: TreeFileInfo | null) => {
- const source = normalizeRecognitionConfigText(item?.funcRegx);
- if (!source) return '';
- const rows = parseRecognitionBasicInfoRows(source);
- if (!rows.length) return source;
- if (!hasRecognitionBasicInfoFilledContent(rows)) return '';
- const payload = rows.reduce(
- (acc, row) => {
- const targetField = resolveRecognitionBasicInfoTargetField(row.targetField);
- const headerMatch = normalizeRecognitionBasicInfoText(row.headerMatch);
- const endFlag = normalizeRecognitionBasicInfoText(row.endFlag);
- const mappedHeader = resolveMappedHeaderByTargetField(item, targetField);
- const nextKey = headerMatch || mappedHeader || targetField;
- if (!nextKey) return acc;
- acc[nextKey] = [headerMatch, endFlag];
- return acc;
- },
- {} as Record<string, string[]>,
- );
- return Object.keys(payload).length ? JSON.stringify(payload) : '';
- };
- const buildImportFileInfoPayload = (item?: TreeFileInfo | null): TreeFileInfo | null => {
- if (!item) return null;
- const nextItem = cloneDeepSafe(item) as TreeFileInfo;
- const children = Array.isArray(nextItem.children)
- ? nextItem.children
- .map((child) => buildImportFileInfoPayload(child))
- .filter((child): child is TreeFileInfo => Boolean(child))
- : nextItem.children;
- if (isFileRootItem(nextItem)) {
- const rootPayload = {
- ...nextItem,
- children,
- } as TreeFileInfo & { childTemplateName?: string | null };
- delete rootPayload.childTemplateName;
- return rootPayload;
- }
- const childName = normalizeRuleText(nextItem.childTemplateName);
- const hasNew = !!childName || !!nextItem.hasNew;
- const importTemplateId = normalizeTemplateId(
- nextItem.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
- );
- const currentTemplate = resolveTemplateInfoById(importTemplateId);
- const parentTemplateId = normalizeTemplateId(nextItem.mainId) || normalizeTemplateId(currentTemplate?.mainId);
- const parentTemplate = (parentTemplateId && resolveTemplateInfoById(parentTemplateId)) || currentTemplate;
- const currentFuncRegx = normalizeRecognitionConfigText(nextItem.funcRegx);
- const importFuncRegx = currentFuncRegx ? buildImportRecognitionBasicInfoFuncRegx(nextItem) : '';
- const sheetPayload = {
- ...nextItem,
- tableName: hasNew ? childName : nextItem.tableName,
- tableNameEn: hasNew
- ? normalizeRuleText(parentTemplate?.tableNameEn) ||
- normalizeRuleText(currentTemplate?.tableNameEn) ||
- normalizeRuleText(nextItem.tableNameEn) ||
- null
- : nextItem.tableNameEn,
- hasNew,
- classify: hasNew
- ? normalizeRuleText(parentTemplate?.classify) ||
- normalizeRuleText(currentTemplate?.classify) ||
- normalizeRuleText(nextItem.classify) ||
- null
- : nextItem.classify,
- funcRegx: importFuncRegx || null,
- children,
- } as TreeFileInfo & { childTemplateName?: string | null };
- return sheetPayload;
- };
- const importFileInfos = sanitizedFileInfos
- .map((item) => buildImportFileInfoPayload(item))
- .filter((item): item is TreeFileInfo => Boolean(item));
- sessionStorage.setItem(IMPORT_FILE_INFO_STORAGE_KEY, JSON.stringify(importFileInfos));
- resetImportProgressSession();
- await go(PageEnum.BASE_CLEAN_PROGRESS);
- } catch {
- createMessage.error('导入数据失败');
- } finally {
- importingData.value = false;
- }
- };
- const loadStoredFileList = () => {
- const raw = sessionStorage.getItem(FILE_INFO_STORAGE_KEY);
- if (!raw) return [];
- try {
- const parsed = JSON.parse(raw) as TreeFileInfo[];
- return Array.isArray(parsed) ? parsed.map((item) => mergeStoredFileInfoWithMock(item)) : [];
- } catch (error) {
- return [];
- }
- };
- const getFocusedNodeKeyFromQuery = () => {
- const rawFocusKey = pageQuery.value?.focusKey;
- return Array.isArray(rawFocusKey) ? normalizeRuleText(rawFocusKey[0]) : normalizeRuleText(rawFocusKey);
- };
- const handleBack = () => {
- // 返回上一级:文件上传页
- go(PageEnum.BASE_UPLOAD);
- };
- const scrollSelectedTreeNodeIntoView = () => {
- void nextTick(() => {
- const selectedNode = document.querySelector(
- '.case-data-cleaning-page .cleaning-file-tree .ant-tree-node-selected',
- );
- const target = (selectedNode?.closest('.ant-tree-treenode') || selectedNode) as HTMLElement | null;
- target?.scrollIntoView({
- block: 'center',
- inline: 'nearest',
- });
- });
- };
- const loadFileTree = async () => {
- const storedList = loadStoredFileList();
- fileList.value = storedList.length ? storedList : sanitizeFileInfoTree(cloneDeepSafe(mockFileList));
- if (!treeData.value.length) return;
- const focusedKey = resolveSelectableTreeKey(getFocusedNodeKeyFromQuery());
- const initialKey =
- focusedKey ||
- (selectedKeys.value.length
- ? resolveSelectableTreeKey(String(selectedKeys.value[0]))
- : findFirstSelectableKey(treeData.value));
- if (!initialKey) return;
- selectedKeys.value = [initialKey];
- await switchNodeState(initialKey);
- if (focusedKey) {
- scrollSelectedTreeNodeIntoView();
- }
- };
- const initializePage = async () => {
- void Promise.all([loadTemplateCategories(), loadTemplateList({ silent: true })]);
- runRulePipeline();
- await loadFileTree();
- };
- onMounted(() => {
- initializePage();
- window.addEventListener('mousemove', onResizeMouseMove);
- window.addEventListener('mouseup', stopResize);
- });
- onBeforeUnmount(() => {
- saveCurrentNodeState();
- window.removeEventListener('mousemove', onResizeMouseMove);
- window.removeEventListener('mouseup', stopResize);
- document.body.classList.remove('is-resizing-layout');
- document.body.style.cursor = '';
- });
- </script>
- <style lang="less">
- // 样式已按域拆分到 ./cleaning/styles/ 下,由此处引入(保持全局,非 scoped)。
- @import './cleaning/styles/index.less';
- </style>
|