| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112 |
- <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 '@jeesite/core/components/EmptyState';
- import { BasicTree } from '@jeesite/core/components/Tree';
- import { useDesign } from '@jeesite/core/hooks/web/useDesign';
- import { useGo, useQuery } from '@jeesite/core/hooks/web/usePage';
- import { PageEnum } from '@jeesite/core/enums/pageEnum';
- import { useMessage } from '@jeesite/core/hooks/web/useMessage';
- import type { FileInfo } from '@jeesite/types/api';
- import { tplCategoryList, tplCheckTableName, tplDetail, tplPage } from '@jeesite/plat/api/tpl/tplApi';
- import type { TableInfo } from '@jeesite/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',
- ): 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;
- };
- 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 createDefaultSourceRows = () => [
- {
- 列A: '本方号码',
- 列B: '使用人姓名',
- 列C: '号码类别',
- 列D: 'IMSI号',
- 列E: 'IMEI号',
- 列F: '归属运营商标识',
- 列G: '号码归属省',
- 列H: '号码归属市',
- },
- {
- 列A: '13800138000',
- 列B: '张伟',
- 列C: '移动',
- 列D: '460001234567890',
- 列E: '869990012345678',
- 列F: '中国移动',
- 列G: '广东省',
- 列H: '深圳市',
- },
- {
- 列A: '13900139001',
- 列B: '李娜',
- 列C: '联通',
- 列D: '460011112223334',
- 列E: '869990045612345',
- 列F: '中国联通',
- 列G: '北京市',
- 列H: '北京市',
- },
- {
- 列A: '13700137002',
- 列B: '王强',
- 列C: '电信',
- 列D: '460031234500987',
- 列E: '869990078900123',
- 列F: '中国电信',
- 列G: '浙江省',
- 列H: '杭州市',
- },
- {
- 列A: '13600136003',
- 列B: '赵敏',
- 列C: '移动',
- 列D: '460001998877665',
- 列E: '869990011122233',
- 列F: '中国移动',
- 列G: '江苏省',
- 列H: '南京市',
- },
- {
- 列A: '13500135004',
- 列B: '陈涛',
- 列C: '联通',
- 列D: '460019876543210',
- 列E: '869990066677788',
- 列F: '中国联通',
- 列G: '四川省',
- 列H: '成都市',
- },
- {
- 列A: '13400134005',
- 列B: '刘洋',
- 列C: '电信',
- 列D: '460031110000222',
- 列E: '869990099988877',
- 列F: '中国电信',
- 列G: '湖北省',
- 列H: '武汉市',
- },
- {
- 列A: '13300133006',
- 列B: '周倩',
- 列C: '移动',
- 列D: '460001010203040',
- 列E: '869990055544433',
- 列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 getElectronIpcRenderer = () => (window as any)?.electron?.ipcRenderer;
- 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> => {
- const normalizedPath = normalizeRuleText(item?.filePath);
- if (!normalizedPath) return null;
- const ipcRenderer = getElectronIpcRenderer();
- if (!ipcRenderer?.invoke) return null;
- try {
- const payload = (await ipcRenderer.invoke('controller/os/readPreviewFile', {
- filePath: normalizedPath,
- headers: normalizeTemplateHeaders(item?.header),
- headerRowIndex: normalizeStoredLineNoToRowIndex(item?.lineNo),
- })) 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('读取预览文件失败', normalizedPath, 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 zf = isTransAmountTargetField(field) ? (recognitionAmountZfEnabled.value ? 1 : 0) : undefined;
- 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>;
- 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 getQueryText = (value?: string | string[] | null) =>
- Array.isArray(value) ? normalizeRuleText(value[0]) : normalizeRuleText(value);
- const handleBack = () => {
- const from = getQueryText(pageQuery.value?.from);
- if (from === 'match') {
- const matchStatus = getQueryText(pageQuery.value?.matchStatus);
- const focusKey = getFocusedNodeKeyFromQuery();
- const matchQuery: Record<string, string> = {};
- if (['all', 'success', 'fail'].includes(matchStatus)) {
- matchQuery.status = matchStatus;
- }
- if (focusKey) {
- matchQuery.focusKey = focusKey;
- }
- go({
- path: PageEnum.BASE_MATCH,
- query: Object.keys(matchQuery).length ? matchQuery : undefined,
- });
- return;
- }
- go(PageEnum.BASE_HOME);
- };
- 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">
- @prefix-cls: ~'jeesite-case-data-cleaning';
- .@{prefix-cls}.case-data-cleaning-page {
- min-height: 0;
- height: calc(100vh - 100px);
- max-height: calc(100vh - 100px);
- background: #fff;
- padding: 12px 16px 24px;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- gap: 12px;
- overflow: hidden;
- .page-header {
- display: flex;
- flex-direction: column;
- gap: 4px;
- padding: 0 2px;
- }
- .page-header-main {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
- }
- .page-title-area {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- min-width: 0;
- flex: 1;
- }
- .page-title-block {
- display: flex;
- flex-direction: column;
- gap: 4px;
- min-width: 0;
- }
- .page-title {
- font-size: 18px;
- line-height: 1.4;
- font-weight: 600;
- color: #111827;
- }
- .page-subtitle {
- font-size: 13px;
- line-height: 1.6;
- color: #6b7280;
- }
- .page-header-actions {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 8px;
- flex-wrap: wrap;
- margin-left: auto;
- :deep(.ant-btn) {
- height: 32px;
- border-radius: 10px;
- }
- }
- .back-btn {
- padding: 0;
- min-width: 32px;
- width: 32px;
- height: 32px;
- border-radius: 10px;
- border: 1px solid #e5e7eb;
- color: #475569;
- background: #f8fafc;
- flex-shrink: 0;
- &:hover {
- color: #2563eb;
- border-color: #bfdbfe;
- background: #eff6ff;
- }
- }
- .main-layout {
- flex: 1 1 auto;
- min-height: 0;
- display: grid;
- gap: 0;
- overflow-x: auto;
- overflow-y: hidden;
- }
- .panel-resizer {
- width: 6px;
- cursor: col-resize;
- background: transparent;
- position: relative;
- }
- .left-panel,
- .middle-panel,
- .right-panel {
- background: #fff;
- border: 1px solid #e5e7eb;
- border-radius: 14px;
- min-height: 0;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- box-shadow: 0 10px 28px rgba(15, 23, 42, 0.04);
- }
- .tree-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 14px 16px 12px;
- border-bottom: 1px solid #eef2f7;
- .label {
- font-size: 15px;
- font-weight: 600;
- color: #111827;
- }
- .actions {
- display: flex;
- gap: 6px;
- }
- }
- .tree-wrap {
- flex: 1;
- min-height: 0;
- overflow: auto;
- padding: 8px;
- .cleaning-file-tree {
- :deep(.ant-tree) {
- margin: 0;
- }
- :deep(.ant-tree-treenode) {
- padding: 2px 0;
- }
- :deep(.ant-tree-node-content-wrapper) {
- min-width: 0;
- border-radius: 10px;
- }
- :deep(.ant-tree-node-content-wrapper.ant-tree-node-selected) {
- background: #eaf1ff;
- }
- :deep(.ant-tree-title) {
- width: 100%;
- min-width: 0;
- }
- .tree-file-row,
- .tree-sheet-row {
- width: 100%;
- min-width: 0;
- display: flex;
- align-items: center;
- box-sizing: border-box;
- }
- .tree-file-row {
- padding: 2px 6px 2px 0;
- }
- .tree-file-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: 14px;
- font-weight: 600;
- color: #1f2937;
- }
- .tree-sheet-row {
- gap: 10px;
- padding: 6px 8px 6px 4px;
- border-radius: 8px;
- }
- .tree-sheet-status {
- width: 18px;
- height: 18px;
- border-radius: 50%;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- font-size: 12px;
- font-weight: 700;
- color: #fff;
- }
- .tree-sheet-row.is-success {
- .tree-sheet-status {
- background: #22c55e;
- }
- }
- .tree-sheet-row.is-fail {
- .tree-sheet-status {
- background: #ef4444;
- }
- }
- .tree-sheet-row.is-analyzing {
- .tree-sheet-status {
- background: linear-gradient(180deg, #60a5fa 0%, #2563eb 100%);
- }
- }
- .tree-sheet-status-spinner {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- border: 2px solid rgba(255, 255, 255, 0.34);
- border-top-color: #ffffff;
- animation: cleaning-sheet-status-spin 0.8s linear infinite;
- box-sizing: border-box;
- }
- .tree-sheet-name {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: 13px;
- color: #334155;
- }
- :deep(.ant-tree-node-content-wrapper.ant-tree-node-selected) {
- .tree-sheet-name {
- color: #2547d0;
- font-weight: 600;
- }
- }
- }
- }
- .legend {
- border-top: 1px solid #eef2f7;
- padding: 10px 12px;
- background: linear-gradient(180deg, #fbfcff 0%, #f7f9fc 100%);
- display: flex;
- flex-direction: column;
- align-items: stretch;
- gap: 6px;
- .legend-item {
- display: flex;
- align-items: center;
- gap: 8px;
- min-height: 20px;
- font-size: 12px;
- color: #475569;
- line-height: 1.4;
- }
- .legend-icon {
- width: 16px;
- text-align: center;
- flex-shrink: 0;
- font-size: 15px;
- font-weight: 700;
- &.is-unmatched {
- color: #cc3333;
- }
- &.is-matched {
- color: #4ba64b;
- }
- &.is-formatError {
- color: #f2a100;
- }
- }
- .legend-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
- display: inline-block;
- background: currentColor;
- }
- .legend-text {
- min-width: 0;
- white-space: nowrap;
- }
- }
- .panel-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
- padding: 18px 18px 10px;
- }
- .panel-title {
- font-size: 15px;
- font-weight: 600;
- color: #111827;
- }
- .panel-desc {
- margin-top: 4px;
- font-size: 12px;
- line-height: 1.6;
- color: #6b7280;
- }
- .right-panel-body {
- flex: 1 1 auto;
- min-height: 0;
- display: grid;
- grid-template-rows: minmax(0, 1fr) auto;
- gap: 12px;
- padding: 12px 16px 16px;
- overflow: hidden;
- }
- .template-match-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 18px 18px 10px;
- border-bottom: 1px solid #eef2f7;
- }
- .template-match-main {
- display: flex;
- align-items: center;
- gap: 12px;
- min-width: 0;
- }
- .template-match-icon {
- width: 28px;
- height: 28px;
- border-radius: 8px;
- background: linear-gradient(135deg, #46b96f 0%, #269656 100%);
- position: relative;
- flex-shrink: 0;
- box-shadow: 0 8px 16px rgba(38, 150, 86, 0.18);
- &::before,
- &::after {
- content: '';
- position: absolute;
- background: rgba(255, 255, 255, 0.92);
- }
- &::before {
- width: 14px;
- height: 2px;
- left: 7px;
- top: 8px;
- box-shadow: 0 5px 0 rgba(255, 255, 255, 0.92);
- }
- &::after {
- width: 2px;
- height: 14px;
- left: 13px;
- top: 7px;
- box-shadow:
- -5px 0 0 rgba(255, 255, 255, 0.92),
- 5px 0 0 rgba(255, 255, 255, 0.92);
- }
- }
- .template-match-copy {
- min-width: 0;
- display: flex;
- align-items: baseline;
- gap: 8px;
- flex-wrap: wrap;
- }
- .template-match-label {
- font-size: 13px;
- font-weight: 600;
- color: #374151;
- white-space: nowrap;
- }
- .template-match-name {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- min-width: 0;
- color: #2643b5;
- font-size: 15px;
- font-weight: 600;
- line-height: 1.4;
- }
- .template-match-badge {
- padding: 2px 8px;
- border-radius: 999px;
- background: #ecf3ff;
- color: #4b63d3;
- font-size: 12px;
- font-weight: 500;
- }
- .template-select-trigger {
- padding: 0;
- font-size: 14px;
- font-weight: 500;
- flex-shrink: 0;
- }
- .template-match-bar.is-empty {
- border-bottom-color: #e8eefb;
- background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
- }
- .template-match-badge.is-pending {
- background: #eef4ff;
- color: #3557d6;
- box-shadow: inset 0 0 0 1px #d9e5ff;
- }
- .middle-panel-body {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- gap: 12px;
- padding: 12px 16px 16px;
- }
- .middle-empty-shell {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- gap: 14px;
- padding: 16px;
- background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
- }
- .middle-empty-stage {
- flex: 1;
- min-height: 360px;
- display: flex;
- flex-direction: column;
- border: 1px solid #e4ebf5;
- border-radius: 18px;
- padding: 16px 18px 18px;
- background:
- radial-gradient(circle at top right, rgba(59, 91, 219, 0.08) 0%, rgba(59, 91, 219, 0) 34%),
- linear-gradient(180deg, #fcfdff 0%, #f8fbff 100%);
- box-shadow:
- inset 0 1px 0 rgba(255, 255, 255, 0.94),
- 0 12px 30px rgba(148, 163, 184, 0.08);
- }
- .middle-empty-stage-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- flex-wrap: wrap;
- }
- .middle-empty-badge {
- display: inline-flex;
- align-items: center;
- height: 28px;
- padding: 0 12px;
- border-radius: 999px;
- background: rgba(47, 79, 210, 0.08);
- color: #2f4fd2;
- font-size: 12px;
- font-weight: 600;
- letter-spacing: 0.02em;
- }
- .middle-empty-sheet {
- color: #64748b;
- font-size: 12px;
- line-height: 1.6;
- }
- .middle-empty-stage-main {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 14px;
- text-align: center;
- }
- .middle-empty-visual {
- position: relative;
- width: 148px;
- height: 108px;
- pointer-events: none;
- &__halo,
- &__card,
- &__dot {
- position: absolute;
- }
- &__halo {
- left: 50%;
- transform: translateX(-50%);
- border-radius: 999px;
- border: 1px solid rgba(137, 160, 255, 0.38);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(233, 241, 255, 0.44) 100%);
- &.is-large {
- bottom: 6px;
- width: 116px;
- height: 34px;
- box-shadow: 0 10px 20px rgba(59, 91, 219, 0.12);
- }
- &.is-small {
- bottom: 24px;
- width: 74px;
- height: 74px;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.98) 0%, rgba(229, 237, 255, 0.52) 72%);
- }
- }
- &__card {
- left: 50%;
- top: 18px;
- width: 52px;
- height: 64px;
- border-radius: 16px;
- border: 1px solid #d5e1ff;
- background: linear-gradient(180deg, #ffffff 0%, #eef4ff 100%);
- box-shadow: 0 12px 24px rgba(59, 91, 219, 0.14);
- transform: translateX(-50%) rotate(-6deg);
- &::before {
- content: '';
- position: absolute;
- left: 10px;
- top: 14px;
- width: 22px;
- height: 3px;
- border-radius: 999px;
- background: #b5c7ff;
- box-shadow: 0 10px 0 #d0dcff;
- }
- &.is-back {
- transform: translateX(-50%) translateX(18px) translateY(6px) rotate(8deg);
- opacity: 0.72;
- box-shadow: 0 8px 18px rgba(148, 163, 184, 0.12);
- }
- }
- &__dot {
- left: 50%;
- bottom: 48px;
- width: 14px;
- height: 14px;
- border-radius: 50%;
- background: linear-gradient(180deg, #3b5bdb 0%, #6d8cff 100%);
- box-shadow: 0 0 0 6px rgba(59, 91, 219, 0.08);
- transform: translateX(-50%);
- }
- }
- .middle-empty-actions {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- }
- .middle-empty-hint {
- max-width: 340px;
- color: #64748b;
- font-size: 12px;
- line-height: 1.7;
- }
- .middle-empty-highlights {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 12px;
- }
- .middle-empty-card {
- min-width: 0;
- padding: 14px;
- border: 1px solid #e6ebf3;
- border-radius: 14px;
- background: #fff;
- box-shadow: 0 8px 22px rgba(148, 163, 184, 0.08);
- &__index {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 30px;
- height: 30px;
- border-radius: 10px;
- background: #eef4ff;
- color: #3557d6;
- font-size: 12px;
- font-weight: 700;
- }
- &__title {
- margin-top: 12px;
- color: #1f2937;
- font-size: 14px;
- font-weight: 600;
- line-height: 1.5;
- }
- &__desc {
- margin-top: 6px;
- color: #64748b;
- font-size: 12px;
- line-height: 1.7;
- }
- }
- .mapping-table-card {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- border: 1px solid #e5eaf2;
- border-radius: 14px;
- overflow: hidden;
- background: #fff;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
- }
- .mapping-card-head {
- padding: 12px 14px;
- border-bottom: 1px solid #edf1f7;
- background: linear-gradient(180deg, #fbfcff 0%, #f6f8fc 100%);
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- }
- .mapping-card-title {
- font-size: 13px;
- font-weight: 600;
- color: #1f2937;
- }
- .mapping-card-actions {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
- }
- .toolbar,
- .mapping-actions {
- margin: 0 16px 0;
- padding: 10px 14px;
- display: flex;
- gap: 8px;
- border: 1px solid #e8edf5;
- border-radius: 10px;
- background: #f8fafc;
- align-items: center;
- flex-wrap: wrap;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
- :deep(.ant-btn) {
- height: 30px;
- padding: 0 12px;
- border-radius: 8px;
- }
- .toolbar-label {
- margin-right: 2px;
- color: #64748b;
- font-size: 12px;
- font-weight: 600;
- white-space: nowrap;
- }
- }
- .toolbar {
- .current-template {
- color: #475569;
- font-size: 12px;
- }
- &--column-actions {
- justify-content: space-between;
- gap: 12px;
- .toolbar-head {
- display: flex;
- align-items: center;
- flex-shrink: 0;
- }
- .toolbar-label {
- margin-right: 0;
- padding-right: 12px;
- position: relative;
- }
- .toolbar-label::after {
- content: '';
- position: absolute;
- right: 0;
- top: 50%;
- width: 1px;
- height: 14px;
- background: #dbe2ea;
- transform: translateY(-50%);
- }
- .toolbar-actions {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- gap: 10px;
- flex: 1;
- flex-wrap: wrap;
- min-width: 0;
- :deep(.ant-btn) {
- min-width: 74px;
- border-color: #c8d7fb;
- background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
- color: #3651d9;
- font-weight: 600;
- box-shadow: none;
- transition: all 0.2s ease;
- }
- :deep(.ant-btn:hover),
- :deep(.ant-btn:focus) {
- border-color: #9fb7fb;
- background: #eef4ff;
- color: #2744c7;
- }
- :deep(.ant-btn:active) {
- border-color: #7f9cf5;
- background: #e2ebff;
- color: #1f3bb4;
- }
- :deep(.ant-btn-primary) {
- border-color: #c8d7fb;
- background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
- color: #3651d9;
- }
- :deep(.ant-btn-primary:hover),
- :deep(.ant-btn-primary:focus),
- :deep(.ant-btn-primary:active) {
- color: #2744c7;
- }
- }
- .toolbar-meta {
- display: inline-flex;
- align-items: center;
- gap: 10px;
- min-height: 34px;
- padding: 0 12px;
- border: 1px dashed #d7e2f3;
- border-radius: 10px;
- background: #fff;
- flex-shrink: 0;
- }
- .meta-label {
- color: #64748b;
- font-size: 12px;
- font-weight: 600;
- line-height: 1;
- white-space: nowrap;
- }
- .meta-input {
- width: 88px;
- :deep(.ant-input) {
- height: 28px;
- padding: 0 10px;
- border-radius: 8px;
- text-align: center;
- font-size: 12px;
- }
- }
- :deep(.ant-btn) {
- min-width: 76px;
- height: 32px;
- padding: 0 14px;
- }
- }
- }
- .mapping-actions {
- margin-top: 12px;
- .mapping-stat {
- margin-left: 4px;
- font-size: 12px;
- color: #334155;
- }
- }
- .mapping-table {
- flex: 1;
- min-height: 0;
- overflow: auto;
- .mapping-head,
- .mapping-row {
- display: grid;
- grid-template-columns: 64px 72px minmax(150px, 1fr) minmax(168px, 220px);
- align-items: center;
- }
- .mapping-head {
- position: sticky;
- top: 0;
- z-index: 1;
- background: #f4f7fb;
- color: #334155;
- font-weight: 600;
- }
- .mapping-row {
- border-top: 1px solid #f1f5f9;
- color: #475569;
- cursor: pointer;
- transition:
- background-color 0.2s ease,
- box-shadow 0.2s ease;
- &:hover {
- background: #fbfdff;
- }
- &.is-active {
- background: #eef4ff;
- box-shadow:
- inset 3px 0 0 #3651d9,
- inset 0 0 0 1px rgba(54, 81, 217, 0.18);
- > span {
- color: #243b7a;
- }
- .mapping-field-trigger {
- border-color: #3651d9;
- box-shadow: 0 0 0 3px rgba(59, 91, 219, 0.08);
- }
- }
- }
- .mapping-head > span,
- .mapping-row > span {
- padding: 8px 12px;
- min-height: 50px;
- font-size: 14px;
- display: flex;
- align-items: center;
- box-sizing: border-box;
- border-right: 1px solid #edf2f7;
- }
- .mapping-head > span:last-child,
- .mapping-row > span:last-child {
- border-right: 0;
- }
- .mapping-head > span:nth-child(1),
- .mapping-head > span:nth-child(2),
- .mapping-row > span:nth-child(1) {
- justify-content: center;
- }
- .mapping-row > span:nth-child(2) {
- justify-content: center;
- }
- .status-cell {
- justify-content: center;
- }
- .mapping-status-group {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- }
- .mapping-status-badge {
- width: 14px;
- height: 14px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- &.is-matched {
- color: #4ba64b;
- }
- &.is-required {
- color: #cc3333;
- }
- &.is-optional {
- color: transparent;
- }
- }
- .mapping-status-dot {
- width: 7px;
- height: 7px;
- border-radius: 50%;
- display: inline-block;
- background: currentColor;
- }
- .mapping-status-error {
- width: 14px;
- height: 14px;
- border-radius: 50%;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex: 0 0 14px;
- background: #f59e0b;
- color: #fff;
- font-size: 10px;
- font-weight: 700;
- box-shadow: 0 4px 10px rgba(245, 158, 11, 0.18);
- }
- .target-field-cell {
- color: #24324b;
- }
- .origin-field-cell {
- padding: 8px 10px;
- }
- .required {
- color: #ef4444;
- margin-right: 4px;
- }
- .mapping-field-trigger {
- width: 100%;
- min-height: 34px;
- padding: 0 12px;
- border: 1px solid #d7dfeb;
- border-radius: 8px;
- background: #fff;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- cursor: pointer;
- transition:
- border-color 0.2s ease,
- box-shadow 0.2s ease;
- &:hover {
- border-color: #b8c5da;
- box-shadow: 0 0 0 3px rgba(59, 91, 219, 0.06);
- }
- &.is-disabled {
- cursor: not-allowed;
- border-color: #e2e8f0;
- background: #f8fafc;
- box-shadow: none;
- &:hover {
- border-color: #e2e8f0;
- box-shadow: none;
- }
- }
- }
- .mapping-field-value {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #334155;
- font-size: 13px;
- &.placeholder {
- color: #9aa4b2;
- }
- }
- .mapping-field-arrow {
- flex-shrink: 0;
- color: #64748b;
- font-size: 12px;
- }
- .mapping-empty {
- padding: 28px 16px;
- text-align: center;
- color: #94a3b8;
- font-size: 13px;
- }
- }
- .template-foot-panel {
- display: flex;
- flex-direction: column;
- flex: 0 0 clamp(344px, 56%, 520px);
- min-height: clamp(344px, 56%, 520px);
- max-height: clamp(344px, 56%, 520px);
- border: 1px solid #e6ebf3;
- border-radius: 14px;
- background: #fff;
- overflow: hidden;
- transition:
- min-height 0.22s ease,
- max-height 0.22s ease,
- flex-basis 0.22s ease;
- &.is-collapsed {
- flex-basis: 156px;
- min-height: 156px;
- max-height: 156px;
- .recognition-panel {
- flex: 0 0 auto;
- padding-bottom: 8px;
- }
- }
- &.is-compact {
- flex-basis: 108px;
- min-height: 108px;
- max-height: 108px;
- }
- }
- .recognition-panel {
- flex: 1 1 auto;
- min-height: 0;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- padding: 14px 16px 12px;
- }
- .recognition-panel-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- min-height: 32px;
- }
- .recognition-title {
- color: #1f2937;
- font-size: 14px;
- font-weight: 600;
- }
- .recognition-toggle {
- color: #3b5bdb;
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- user-select: none;
- white-space: nowrap;
- display: inline-flex;
- align-items: center;
- gap: 4px;
- }
- .recognition-toggle-icon {
- font-size: 11px;
- line-height: 1;
- }
- .recognition-panel-body {
- flex: 1 1 auto;
- min-height: 0;
- overflow: auto;
- display: flex;
- flex-direction: column;
- gap: 12px;
- margin-top: 12px;
- padding-right: 4px;
- }
- .recognition-tabs {
- display: flex;
- align-items: center;
- gap: 16px;
- flex-wrap: wrap;
- }
- .recognition-tab {
- position: relative;
- color: #64748b;
- font-size: 13px;
- cursor: pointer;
- transition: color 0.2s ease;
- &::after {
- content: '';
- position: absolute;
- left: 0;
- right: 0;
- bottom: -6px;
- height: 2px;
- border-radius: 999px;
- background: transparent;
- }
- &.active {
- color: #2f4fd2;
- font-weight: 600;
- &::after {
- background: #2f4fd2;
- }
- }
- }
- .recognition-desc {
- padding: 10px 12px;
- border-radius: 10px;
- background: #f7faff;
- color: #64748b;
- font-size: 12px;
- line-height: 1.7;
- }
- .recognition-basic-shell {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .recognition-basic-preview,
- .recognition-basic-panel,
- .recognition-basic-empty-shell,
- .recognition-rule-placeholder {
- border: 1px solid #dce6f3;
- border-radius: 12px;
- background: linear-gradient(180deg, #f9fbff 0%, #ffffff 100%);
- box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
- }
- .recognition-basic-preview {
- padding: 12px 14px;
- }
- .recognition-basic-preview__head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 10px;
- }
- .recognition-basic-preview__title {
- color: #1f2937;
- font-size: 13px;
- font-weight: 700;
- }
- .recognition-basic-preview__meta {
- display: inline-flex;
- align-items: center;
- height: 24px;
- padding: 0 10px;
- border-radius: 999px;
- background: #eef4ff;
- color: #315efb;
- font-size: 12px;
- font-weight: 600;
- white-space: nowrap;
- }
- .recognition-basic-preview__content {
- min-height: 52px;
- padding: 10px 12px;
- border-radius: 10px;
- background: #ffffff;
- color: #475569;
- font-size: 12px;
- line-height: 1.6;
- white-space: nowrap;
- word-break: normal;
- overflow-x: auto;
- overflow-y: hidden;
- border: 1px dashed #d7e2f3;
- }
- .recognition-basic-panel {
- padding: 12px;
- }
- .recognition-basic-panel__head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 10px;
- }
- .recognition-basic-panel__summary {
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .recognition-basic-panel__title-wrap {
- display: flex;
- align-items: center;
- gap: 10px;
- flex-wrap: wrap;
- }
- .recognition-basic-panel__title {
- color: #111827;
- font-size: 15px;
- font-weight: 700;
- }
- .recognition-basic-panel__count {
- display: inline-flex;
- align-items: center;
- height: 24px;
- padding: 0 10px;
- border-radius: 999px;
- background: #eef4ff;
- color: #315efb;
- font-size: 12px;
- font-weight: 700;
- line-height: 24px;
- }
- .recognition-basic-panel__desc {
- color: #6b7280;
- font-size: 12px;
- line-height: 1.6;
- }
- .recognition-basic-panel__actions {
- display: flex;
- align-items: center;
- gap: 0;
- flex-shrink: 0;
- margin-left: auto;
- }
- .recognition-basic-panel__toggle-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- min-width: 76px;
- height: 36px;
- padding: 0 14px;
- border-radius: 10px;
- border: 1px solid #d7e4f3;
- background: #fff;
- color: #315efb;
- font-weight: 600;
- line-height: 1;
- box-shadow: 0 4px 12px rgba(49, 94, 251, 0.08);
- transition:
- border-color 0.2s ease,
- background-color 0.2s ease,
- box-shadow 0.2s ease,
- transform 0.2s ease;
- &:hover,
- &:focus {
- border-color: #9db8ff;
- background: #f7faff;
- box-shadow: 0 6px 16px rgba(49, 94, 251, 0.14);
- }
- &:active {
- transform: translateY(1px);
- }
- :deep(.ant-btn-icon) {
- display: inline-flex;
- align-items: center;
- }
- }
- .recognition-basic-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- max-height: 220px;
- overflow: auto;
- padding-right: 2px;
- }
- .recognition-basic-item {
- position: relative;
- display: grid;
- grid-template-columns: 48px minmax(0, 1fr);
- align-items: center;
- gap: 10px;
- padding: 10px 12px;
- border: 1px solid #dce5f1;
- border-radius: 12px;
- background: #fff;
- box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
- }
- .recognition-basic-item__index {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 48px;
- height: 40px;
- border-radius: 10px;
- background: linear-gradient(180deg, #f6f9fd 0%, #eef4ff 100%);
- color: #315efb;
- font-size: 13px;
- font-weight: 700;
- line-height: 1;
- }
- .recognition-basic-item__content {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- align-items: center;
- gap: 10px;
- min-width: 0;
- }
- .recognition-basic-field {
- display: flex;
- flex-direction: column;
- gap: 6px;
- min-width: 0;
- :deep(.ant-select),
- :deep(.ant-input-affix-wrapper),
- :deep(.ant-input) {
- width: 100%;
- }
- :deep(.ant-input),
- :deep(.ant-input-affix-wrapper),
- :deep(.ant-select-selector) {
- border-radius: 8px !important;
- }
- }
- .recognition-basic-field__label {
- color: #475569;
- font-size: 12px;
- font-weight: 600;
- line-height: 1.4;
- }
- .recognition-basic-field__readonly {
- display: flex;
- align-items: center;
- min-height: 32px;
- padding: 0 12px;
- border: 1px solid #d7e4f3;
- border-radius: 8px;
- background: linear-gradient(180deg, #f8fbff 0%, #f3f7ff 100%);
- color: #1f2937;
- font-size: 13px;
- font-weight: 500;
- line-height: 1.5;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- &.is-empty {
- color: #94a3b8;
- }
- }
- .recognition-zf-shell {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .recognition-zf-card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- padding: 14px 16px;
- border: 1px solid #dce6f3;
- border-radius: 12px;
- background: linear-gradient(180deg, #f9fbff 0%, #ffffff 100%);
- box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
- }
- .recognition-zf-card__main {
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .recognition-zf-card__title {
- color: #111827;
- font-size: 15px;
- font-weight: 700;
- line-height: 1.5;
- }
- .recognition-zf-card__desc {
- color: #6b7280;
- font-size: 12px;
- line-height: 1.7;
- }
- .recognition-direction-shell {
- display: flex;
- flex-direction: column;
- gap: 12px;
- padding: 14px 16px;
- border: 1px solid #dce6f3;
- border-radius: 12px;
- background: linear-gradient(180deg, #f9fbff 0%, #ffffff 100%);
- box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
- }
- .recognition-direction-shell__head {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
- }
- .recognition-direction-shell__summary {
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .recognition-direction-shell__title {
- color: #111827;
- font-size: 15px;
- font-weight: 700;
- }
- .recognition-direction-shell__desc {
- color: #6b7280;
- font-size: 12px;
- line-height: 1.7;
- }
- .recognition-direction-shell__count {
- display: inline-flex;
- align-items: center;
- height: 24px;
- padding: 0 10px;
- border-radius: 999px;
- background: #eef4ff;
- color: #315efb;
- font-size: 12px;
- font-weight: 700;
- white-space: nowrap;
- }
- .recognition-direction-card-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .recognition-direction-card {
- padding: 12px;
- border: 1px solid #dce5f1;
- border-radius: 12px;
- background: #fff;
- box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
- }
- .recognition-direction-card__head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- margin-bottom: 10px;
- }
- .recognition-direction-card__title {
- color: #1f2937;
- font-size: 13px;
- font-weight: 700;
- line-height: 1.5;
- }
- .recognition-direction-card__tag {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- height: 24px;
- padding: 0 10px;
- border-radius: 999px;
- background: #f3f6fb;
- color: #475569;
- font-size: 12px;
- font-weight: 600;
- white-space: nowrap;
- }
- .recognition-direction-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .recognition-direction-row {
- display: grid;
- grid-template-columns: 72px minmax(0, 1fr);
- align-items: center;
- gap: 10px;
- min-width: 0;
- :deep(.ant-input),
- :deep(.ant-input-affix-wrapper) {
- width: 100%;
- border-radius: 8px !important;
- }
- }
- .recognition-direction-label {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-height: 32px;
- padding: 0 10px;
- border-radius: 8px;
- background: #f3f6fb;
- color: #475569;
- font-size: 12px;
- font-weight: 600;
- line-height: 1;
- white-space: nowrap;
- }
- .recognition-direction-actions {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- padding-top: 4px;
- }
- .recognition-basic-empty,
- .recognition-basic-empty-shell,
- .recognition-rule-placeholder {
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 8px;
- }
- .recognition-basic-empty {
- padding: 12px 2px 2px;
- }
- .recognition-basic-empty-shell,
- .recognition-rule-placeholder {
- padding: 14px 16px;
- }
- .recognition-basic-empty__title,
- .recognition-basic-empty-shell__title,
- .recognition-rule-placeholder__title {
- color: #1f2937;
- font-size: 13px;
- font-weight: 700;
- line-height: 1.5;
- }
- .recognition-basic-empty__desc,
- .recognition-basic-empty-shell__desc,
- .recognition-rule-placeholder__desc {
- color: #64748b;
- font-size: 12px;
- line-height: 1.7;
- }
- .child-template-panel {
- flex: 0 0 auto;
- padding: 12px 16px 14px;
- border-top: 1px solid #eef2f7;
- background: #fcfdff;
- }
- .template-foot-panel.is-compact {
- .child-template-panel {
- padding-top: 14px;
- border-top: 0;
- }
- }
- .child-template-tip {
- color: #64748b;
- font-size: 12px;
- line-height: 1.7;
- }
- .child-template-form {
- margin-top: 12px;
- display: flex;
- align-items: center;
- gap: 10px;
- :deep(.ant-input-affix-wrapper),
- :deep(.ant-input) {
- flex: 1;
- }
- }
- .child-template-label {
- color: #374151;
- font-size: 14px;
- white-space: nowrap;
- }
- .cleaning-rule-panel {
- margin: 0;
- border: 1px solid #e5e7eb;
- border-radius: 14px;
- padding: 12px;
- background: #fff;
- display: flex;
- flex-direction: column;
- min-height: 0;
- flex-shrink: 0;
- overflow: hidden;
- transition:
- padding 0.2s ease,
- border-color 0.2s ease,
- box-shadow 0.2s ease;
- &.is-collapsed {
- padding-bottom: 10px;
- .rule-title-row {
- margin-bottom: 0;
- }
- }
- .rule-title-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- flex-wrap: wrap;
- margin-bottom: 10px;
- min-height: 36px;
- flex: 0 0 auto;
- .rule-tabs {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
- .rule-tab {
- min-width: 82px;
- height: 34px;
- padding: 0 14px;
- border: 1px solid #e5e7eb;
- border-radius: 999px;
- background: #fff;
- color: #475569;
- cursor: pointer;
- transition: all 0.2s ease;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- &.active {
- border-color: #c8d7fb;
- background: #eef4ff;
- color: #3651d9;
- }
- }
- }
- .rule-title-meta {
- margin-left: auto;
- display: inline-flex;
- align-items: center;
- justify-content: flex-end;
- gap: 12px;
- flex-wrap: wrap;
- }
- .current-col {
- font-size: 12px;
- color: #64748b;
- }
- .rule-toggle {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- gap: 4px;
- height: 30px;
- padding: 0 12px;
- border: 1px solid #dbe4f0;
- border-radius: 999px;
- background: #fff;
- color: #475569;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- user-select: none;
- transition: all 0.2s ease;
- &:hover,
- &.expanded {
- border-color: #c8d7fb;
- background: #eef4ff;
- color: #3651d9;
- }
- }
- .rule-toggle-icon {
- font-size: 11px;
- line-height: 1;
- }
- }
- .rule-content {
- display: grid;
- grid-template-columns: minmax(0, 0.95fr) 54px minmax(0, 0.95fr) minmax(360px, 1.55fr);
- gap: 12px;
- height: 392px;
- min-height: 392px;
- align-items: stretch;
- .rule-col,
- .rule-config {
- border: 1px solid #e5e7eb;
- border-radius: 10px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- min-height: 0;
- }
- .col-title {
- height: 36px;
- line-height: 36px;
- padding: 0 10px;
- background: #f8fafc;
- font-size: 13px;
- color: #334155;
- font-weight: 600;
- &.between {
- display: flex;
- justify-content: space-between;
- }
- .clear-btn {
- color: #ef4444;
- cursor: pointer;
- font-weight: 400;
- }
- }
- .col-list {
- flex: 1;
- min-height: 0;
- overflow: auto;
- padding: 4px 0;
- .col-item {
- height: 32px;
- padding: 0 10px;
- cursor: pointer;
- color: #475569;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- &:hover {
- background: #f8fafc;
- }
- &.active {
- color: #3651d9;
- background: #e8ebfb;
- }
- }
- .col-item-label {
- min-width: 0;
- flex: 1 1 auto;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .col-item-status {
- flex: 0 0 auto;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 52px;
- height: 20px;
- padding: 0 8px;
- border-radius: 999px;
- font-size: 11px;
- font-weight: 600;
- line-height: 1;
- &.is-enabled {
- background: #e8f8f1;
- color: #0f766e;
- }
- &.is-disabled {
- background: #f1f5f9;
- color: #64748b;
- }
- }
- }
- .rule-op {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 8px;
- :deep(.ant-btn) {
- width: 32px;
- height: 28px;
- padding: 0;
- border-radius: 8px;
- }
- }
- .config-placeholder {
- flex: 1;
- min-height: 0;
- overflow: auto;
- padding: 12px;
- font-size: 13px;
- color: #64748b;
- background: #f8fbff;
- display: flex;
- flex-direction: column;
- .config-rule-shell {
- display: flex;
- flex-direction: column;
- gap: 12px;
- min-height: 100%;
- &.is-example-preview {
- justify-content: center;
- min-height: auto;
- margin: auto 0;
- }
- }
- .config-rule-head,
- .config-section {
- border: 1px solid #dbe7f6;
- border-radius: 12px;
- background: #fff;
- box-shadow: 0 6px 18px rgba(148, 163, 184, 0.08);
- }
- .config-rule-head {
- padding: 14px 16px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .config-rule-title-row {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
- }
- .config-rule-copy {
- min-width: 0;
- flex: 1 1 auto;
- }
- .config-rule-title {
- color: #0f172a;
- font-size: 15px;
- font-weight: 700;
- line-height: 1.4;
- }
- .config-rule-desc {
- margin-top: 6px;
- color: #64748b;
- line-height: 1.7;
- }
- .config-rule-meta {
- display: inline-flex;
- align-items: center;
- justify-content: flex-end;
- gap: 8px;
- flex-wrap: wrap;
- flex-shrink: 0;
- }
- .config-rule-state {
- display: inline-flex;
- align-items: center;
- height: 28px;
- padding: 0 10px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- &.is-enabled {
- background: #e8f8f1;
- color: #0f766e;
- }
- &.is-disabled {
- background: #f1f5f9;
- color: #64748b;
- }
- &.is-example {
- background: #eff6ff;
- color: #2563eb;
- }
- }
- .config-rule-tag {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- height: 28px;
- padding: 0 10px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- &.is-shared {
- background: #eef2ff;
- color: #4f46e5;
- }
- &.is-column {
- background: #e9f8f4;
- color: #0f766e;
- }
- }
- .config-rule-toggle-btn {
- border-radius: 999px;
- }
- .config-section {
- padding: 14px 16px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- &.is-example {
- background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
- }
- }
- .config-section-title {
- color: #1e293b;
- font-size: 13px;
- font-weight: 700;
- line-height: 1.4;
- }
- .config-form-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 10px;
- }
- .config-field {
- display: flex;
- flex-direction: column;
- gap: 6px;
- min-width: 0;
- }
- .config-field-label {
- color: #475569;
- font-size: 12px;
- font-weight: 600;
- line-height: 1.4;
- }
- .config-hint,
- .config-desc-item {
- color: #64748b;
- line-height: 1.7;
- }
- .config-desc-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- .config-check-list {
- display: flex;
- flex-wrap: wrap;
- gap: 10px 16px;
- &.single {
- gap: 0;
- }
- }
- .config-check-inline {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- gap: 10px;
- align-items: center;
- }
- .config-extract-data-group {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px;
- }
- .config-extract-data-item {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr) auto;
- gap: 8px;
- align-items: center;
- :deep(.ant-radio-wrapper) {
- margin-inline-end: 0;
- white-space: nowrap;
- }
- }
- .config-extract-data-unit {
- color: #475569;
- white-space: nowrap;
- }
- .config-radio-group {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- &.compact {
- gap: 4px 12px;
- }
- :deep(.ant-radio-wrapper) {
- margin-inline-end: 0;
- }
- }
- .config-alert {
- padding: 10px 12px;
- border-radius: 10px;
- background: #fff7ed;
- border: 1px solid #fed7aa;
- color: #c2410c;
- line-height: 1.6;
- }
- .config-actions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- &.between {
- justify-content: space-between;
- }
- }
- .grouping-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .grouping-row {
- display: grid;
- grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(120px, 0.8fr) auto;
- gap: 10px;
- align-items: end;
- }
- .grouping-remove-btn {
- align-self: end;
- }
- .config-example-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .config-example-row {
- display: grid;
- grid-template-columns: 72px minmax(0, 1fr);
- gap: 10px;
- align-items: start;
- color: #475569;
- line-height: 1.7;
- span:first-child {
- color: #64748b;
- font-weight: 600;
- }
- code {
- padding: 4px 8px;
- border-radius: 8px;
- background: #eff6ff;
- color: #1d4ed8;
- word-break: break-all;
- }
- }
- .config-preview-tip {
- padding: 10px 12px;
- border: 1px dashed #c7d8f6;
- border-radius: 10px;
- background: #fff;
- color: #64748b;
- line-height: 1.7;
- }
- :deep(.ant-select),
- :deep(.ant-input-number) {
- width: 100%;
- }
- :deep(.ant-input-number) {
- border-radius: 8px;
- }
- .config-empty-state {
- margin-top: auto;
- margin-bottom: auto;
- padding: 14px;
- border: 1px dashed #d7e2f3;
- border-radius: 10px;
- background: #fff;
- }
- .empty-title {
- color: #111827;
- font-size: 14px;
- font-weight: 600;
- }
- .empty-text {
- margin-top: 8px;
- color: #475569;
- line-height: 1.7;
- }
- .empty-tip {
- margin-top: 8px;
- color: #6b7280;
- line-height: 1.7;
- }
- }
- @media (max-width: 1680px) {
- grid-template-columns: minmax(0, 1fr) 48px minmax(0, 1fr);
- height: auto;
- min-height: 0;
- .rule-config {
- grid-column: 1 / -1;
- min-height: 340px;
- }
- }
- @media (max-width: 1320px) {
- .config-placeholder {
- .config-form-grid,
- .grouping-row {
- grid-template-columns: 1fr;
- }
- .config-actions.between {
- flex-direction: column;
- align-items: stretch;
- }
- .grouping-remove-btn {
- width: 100%;
- }
- }
- }
- }
- @media (max-width: 1320px) {
- .rule-title-row {
- align-items: flex-start;
- .rule-title-meta {
- width: 100%;
- justify-content: space-between;
- }
- }
- }
- }
- .table-mock {
- margin: 0;
- border: 1px solid #e5e7eb;
- border-radius: 14px;
- min-height: 0;
- height: 100%;
- overflow: auto;
- overscroll-behavior: contain;
- .mock-head,
- .mock-match-row,
- .mock-row {
- display: grid;
- width: max-content;
- min-width: max-content;
- }
- .mock-head {
- background: #f8fafc;
- color: #334155;
- font-weight: 600;
- > span {
- gap: 8px;
- }
- span {
- cursor: pointer;
- }
- span.is-added {
- color: #3651d9;
- }
- span.active {
- background: #eef4ff;
- color: #3651d9;
- }
- }
- .mock-match-row {
- border-top: 1px solid #e5e7eb;
- background: #f8fbff;
- color: #334155;
- > span {
- border-top: 0;
- }
- .match-field-content {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- width: auto;
- max-width: 100%;
- min-width: 0;
- overflow: hidden;
- }
- .match-status-group {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- flex: 0 0 auto;
- }
- .match-status-badge {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex: 0 0 16px;
- color: #fff;
- font-size: 11px;
- font-weight: 700;
- &.is-success {
- background: #22c55e;
- }
- &.is-fail {
- background: #ef4444;
- }
- &.is-formatError {
- background: #f59e0b;
- }
- }
- .match-field-text {
- color: #3651d9;
- font-weight: 600;
- display: block;
- flex: 0 1 auto;
- min-width: 0;
- max-width: calc(100% - 22px);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- span.is-empty .match-field-text {
- color: #94a3b8;
- font-weight: 500;
- }
- > span.is-empty .match-field-content {
- width: auto;
- min-width: auto;
- overflow: visible;
- }
- > span.is-empty .match-field-text {
- flex: 0 0 auto;
- max-width: none;
- }
- > span:not(.is-empty) .match-field-content {
- padding-right: 0;
- }
- > span:not(.is-empty) .match-status-group {
- position: static;
- transform: none;
- }
- }
- .mock-row {
- border-top: 1px solid #f1f5f9;
- color: #475569;
- position: relative;
- transition:
- box-shadow 0.2s ease,
- background-color 0.2s ease;
- &::after {
- content: '';
- position: absolute;
- inset: 0;
- background: rgba(54, 81, 217, 0.14);
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.2s ease;
- }
- &.is-current-row {
- background: #eef4ff;
- box-shadow:
- inset 3px 0 0 #3651d9,
- inset 0 0 0 1px rgba(54, 81, 217, 0.24);
- &::after {
- opacity: 1;
- }
- span {
- background: transparent;
- box-shadow: none;
- color: inherit;
- }
- }
- &.is-header-row {
- background: #f3f7ff;
- box-shadow:
- inset 0 1px 0 rgba(54, 81, 217, 0.16),
- inset 0 -1px 0 rgba(54, 81, 217, 0.16);
- span {
- font-weight: 600;
- color: #274690;
- }
- }
- }
- .mock-row.is-header-row.is-current-row {
- background: #e8f0ff;
- }
- .mock-row.is-ignored-row {
- background: #5f6368;
- color: #fff;
- }
- .mock-row.is-ignored-row.is-header-row {
- background: #48505d;
- span {
- color: #fff;
- }
- }
- .mock-head > span,
- .mock-match-row > span,
- .mock-row > span {
- padding: 10px 12px;
- font-size: 12px;
- cursor: pointer;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
- min-height: 44px;
- display: flex;
- align-items: center;
- position: relative;
- z-index: 1;
- box-sizing: border-box;
- transition:
- background-color 0.2s ease,
- box-shadow 0.2s ease,
- color 0.2s ease;
- }
- .row-index-cell {
- justify-content: center;
- text-align: center;
- color: #64748b;
- font-weight: 600;
- }
- .row-index-head {
- color: #475569;
- background: #f8fafc;
- }
- .match-row-index {
- background: #f8fbff;
- }
- .rule-badge {
- position: relative;
- flex: 0 0 auto;
- width: 24px;
- height: 14px;
- margin-left: 8px;
- border-radius: 999px;
- background: linear-gradient(180deg, #eef4ff 0%, #e1ebff 100%);
- box-shadow: inset 0 0 0 1px rgba(54, 81, 217, 0.14);
- &::before {
- content: '';
- position: absolute;
- left: 5px;
- right: 5px;
- top: 50%;
- height: 2px;
- border-radius: 999px;
- background: currentColor;
- opacity: 0.38;
- transform: translateY(-50%);
- }
- &::after {
- content: '';
- position: absolute;
- left: 4px;
- top: 50%;
- width: 4px;
- height: 4px;
- border-radius: 50%;
- background: currentColor;
- transform: translateY(-50%);
- box-shadow:
- 6px 0 0 currentColor,
- 12px 0 0 currentColor;
- }
- &.is-active {
- color: #16a34a;
- background: linear-gradient(180deg, #edfdf3 0%, #ddf8e8 100%);
- box-shadow: inset 0 0 0 1px rgba(22, 163, 74, 0.18);
- }
- &.is-draft {
- color: #64748b;
- background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
- box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.22);
- }
- }
- .mock-head span.is-current-col {
- background: #dfe8ff;
- box-shadow:
- inset 0 0 0 1px #3651d9,
- inset 0 -2px 0 #3651d9;
- color: #3651d9;
- }
- .mock-head span.is-current-col.is-first-col {
- border-top-left-radius: 14px;
- }
- .mock-head span.is-current-col.is-last-col {
- border-top-right-radius: 14px;
- }
- .mock-row span.is-current-col {
- background: rgba(54, 81, 217, 0.16) !important;
- box-shadow:
- inset 1px 0 0 rgba(54, 81, 217, 0.24),
- inset -1px 0 0 rgba(54, 81, 217, 0.24) !important;
- color: #243b7a !important;
- }
- .mock-match-row span.is-current-col {
- background: rgba(54, 81, 217, 0.12);
- box-shadow:
- inset 1px 0 0 rgba(54, 81, 217, 0.24),
- inset -1px 0 0 rgba(54, 81, 217, 0.24);
- }
- .mock-row.is-current-row span.is-current-col {
- background: rgba(54, 81, 217, 0.22) !important;
- box-shadow:
- inset 1px 0 0 rgba(54, 81, 217, 0.28),
- inset -1px 0 0 rgba(54, 81, 217, 0.28) !important;
- color: #1f3fbf !important;
- }
- .mock-row.is-ignored-row span.is-current-col {
- background: rgba(255, 255, 255, 0.12) !important;
- box-shadow:
- inset 1px 0 0 rgba(255, 255, 255, 0.2),
- inset -1px 0 0 rgba(255, 255, 255, 0.2) !important;
- color: #fff !important;
- }
- }
- @media (max-width: 1180px) {
- .page-header-main {
- flex-direction: column;
- }
- .page-header-actions {
- width: 100%;
- margin-left: 0;
- justify-content: flex-start;
- }
- .template-match-bar,
- .mapping-card-head,
- .child-template-form {
- align-items: flex-start;
- flex-direction: column;
- }
- .middle-empty-stage-head {
- align-items: flex-start;
- flex-direction: column;
- }
- .middle-empty-stage {
- min-height: 320px;
- }
- .template-foot-panel {
- flex-basis: clamp(316px, 54%, 480px);
- min-height: clamp(316px, 54%, 480px);
- max-height: clamp(316px, 54%, 480px);
- &.is-collapsed {
- flex-basis: 208px;
- min-height: 208px;
- max-height: 208px;
- }
- &.is-compact {
- flex-basis: 116px;
- min-height: 116px;
- max-height: 116px;
- }
- }
- .middle-empty-highlights {
- grid-template-columns: 1fr;
- }
- .right-panel-body {
- gap: 10px;
- padding: 10px 12px 12px;
- }
- .toolbar--column-actions {
- align-items: flex-start;
- .toolbar-head,
- .toolbar-actions,
- .toolbar-meta {
- width: 100%;
- }
- .toolbar-label {
- padding-right: 0;
- }
- .toolbar-label::after {
- display: none;
- }
- .toolbar-meta {
- justify-content: flex-start;
- }
- }
- .recognition-basic-panel__head,
- .recognition-basic-panel__actions,
- .recognition-basic-preview__head,
- .recognition-zf-card,
- .recognition-direction-shell__head {
- align-items: flex-start;
- flex-direction: column;
- }
- .recognition-basic-panel__actions,
- .recognition-direction-actions {
- width: auto;
- justify-content: flex-end;
- }
- .recognition-basic-item {
- grid-template-columns: 1fr;
- }
- .recognition-basic-item__index {
- width: 40px;
- }
- .recognition-basic-item__content {
- grid-template-columns: 1fr;
- }
- .recognition-direction-row {
- grid-template-columns: 1fr;
- }
- .recognition-direction-label {
- justify-content: flex-start;
- }
- }
- }
- // Modal 样式需要在 .case-data-cleaning-page 外部,因为 Modal 会渲染到 body 下
- .cleaning-config-modal {
- .ant-modal-content {
- border-radius: 14px;
- padding: 0;
- overflow: hidden;
- border: 1px solid #e5e7eb;
- box-shadow: 0 18px 40px rgba(15, 23, 42, 0.14);
- }
- .ant-modal-header {
- padding: 12px 16px;
- border-bottom: 1px solid #e5e7eb;
- background: @modal-header-bg-color;
- }
- .ant-modal-title {
- color: #111827;
- font-size: 16px;
- font-weight: 600;
- line-height: 24px;
- }
- .ant-modal-body {
- padding: 16px;
- background: #fff;
- }
- .ant-modal-footer {
- padding: 8px 16px 10px;
- border-top: 1px solid #e5e7eb;
- background: #fff;
- }
- .ant-btn {
- min-width: 72px;
- height: 36px;
- padding: 0 18px;
- border-radius: 18px;
- border-width: 1px;
- font-size: 14px;
- font-weight: 500;
- }
- .ant-btn-primary {
- box-shadow: none;
- }
- .ant-input,
- .ant-input-affix-wrapper,
- .ant-select-selector {
- border-radius: 8px !important;
- }
- }
- .cleaning-modal-title {
- display: flex;
- flex-direction: column;
- gap: 4px;
- .title-main {
- font-size: 16px;
- font-weight: 600;
- color: #111827;
- }
- .title-desc {
- font-size: 12px;
- line-height: 1.6;
- color: #6b7280;
- font-weight: 400;
- }
- }
- .split-column-modal {
- .ant-modal {
- width: calc(100vw - 32px) !important;
- max-width: 460px;
- }
- }
- .split-column-form {
- display: flex;
- flex-direction: column;
- gap: 12px;
- .split-column-tip {
- padding: 10px 12px;
- border: 1px dashed #d6deee;
- border-radius: 10px;
- background: #f8fbff;
- color: #64748b;
- font-size: 12px;
- line-height: 1.6;
- }
- .split-form-card {
- padding: 16px 16px 2px;
- border: 1px solid #e8edf5;
- border-radius: 12px;
- background: #f8fafc;
- }
- .form-row {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 14px;
- }
- .form-label {
- width: 126px;
- flex-shrink: 0;
- color: #374151;
- font-size: 14px;
- text-align: right;
- &.required::before {
- content: '*';
- color: #ff4d4f;
- margin-right: 4px;
- }
- }
- .form-input {
- flex: 1;
- max-width: 292px;
- }
- .delimiter-group {
- display: flex;
- gap: 8px;
- flex: 1;
- max-width: 292px;
- }
- .delimiter-select {
- width: 120px;
- flex-shrink: 0;
- }
- .delimiter-input {
- flex: 1;
- min-width: 0;
- }
- .form-row:last-child {
- margin-bottom: 0;
- }
- }
- .add-column-modal {
- .ant-modal {
- width: calc(100vw - 32px) !important;
- max-width: 704px;
- }
- }
- .add-col-dialog {
- .label.required::before {
- content: '*';
- color: #ef4444;
- margin-right: 4px;
- }
- &.addColumn {
- .addColumn-name {
- margin-bottom: 16px;
- padding: 14px 16px;
- border: 1px solid #e8edf5;
- border-radius: 12px;
- background: #f8fafc;
- .row {
- display: flex;
- align-items: center;
- gap: 12px;
- width: 100%;
- flex-wrap: nowrap;
- }
- .label {
- width: 92px;
- min-width: 92px;
- flex: 0 0 92px;
- color: #374151;
- font-size: 14px;
- line-height: 32px;
- font-weight: 500;
- white-space: nowrap;
- &::before {
- content: '*';
- color: #e65050;
- margin-right: 4px;
- }
- }
- .name-input {
- flex: 1 1 auto;
- min-width: 0;
- }
- .name-input:deep(.ant-input-affix-wrapper),
- .name-input:deep(.ant-input) {
- width: 100%;
- height: 32px;
- line-height: 32px;
- border-radius: 4px;
- font-size: 14px;
- }
- }
- .addColumn-tip {
- margin-bottom: 12px;
- padding: 10px 12px;
- border: 1px dashed #d6deee;
- border-radius: 10px;
- background: #f8fbff;
- color: #64748b;
- font-size: 12px;
- line-height: 1.6;
- }
- .addColumn-content {
- display: flex;
- gap: 18px;
- height: 404px;
- .addColumn-content-left,
- .addColumn-content-right {
- display: flex;
- flex-direction: column;
- min-width: 0;
- padding: 16px;
- border: 1px solid #e8edf5;
- border-radius: 12px;
- background: #fff;
- }
- .addColumn-content-left {
- flex: 0 0 42%;
- }
- .addColumn-content-right {
- flex: 1 1 58%;
- }
- .content-card-head {
- display: flex;
- flex-direction: column;
- gap: 4px;
- min-width: 0;
- }
- .content-left-tip,
- .content-right-tip {
- color: #111827;
- font-size: 14px;
- font-weight: 600;
- line-height: 22px;
- }
- .content-card-desc {
- color: #6b7280;
- font-size: 12px;
- line-height: 1.5;
- }
- .left-oldColumn,
- .right-newColumn {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- border: 1px solid #e5e7eb;
- border-radius: 10px;
- margin-top: 12px;
- background: #f8fafc;
- padding: 6px 0;
- }
- .left-oldColumn::-webkit-scrollbar,
- .right-newColumn::-webkit-scrollbar {
- width: 8px;
- }
- .left-oldColumn::-webkit-scrollbar-thumb,
- .right-newColumn::-webkit-scrollbar-thumb {
- background: #9ca3af;
- border-radius: 999px;
- }
- .oldColumn-box,
- .newColumn-box {
- min-height: 36px;
- padding: 0 14px;
- color: #475569;
- font-size: 14px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- border: 1px solid transparent;
- border-radius: 8px;
- margin: 0 8px 4px;
- transition: all 0.2s ease;
- &:hover {
- background: #f3f6fb;
- color: #1f2937;
- }
- &.active-item {
- background: #eef4ff;
- color: #1d4ed8;
- font-weight: 600;
- }
- }
- .oldColumn-box {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .right-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 12px;
- .content-card-head {
- flex: 1;
- }
- }
- .right-radio {
- margin: 12px 0 10px;
- padding: 10px 12px;
- border: 1px solid #e8edf5;
- border-radius: 10px;
- background: #f8fafc;
- :deep(.ant-radio-wrapper) {
- margin-right: 24px;
- color: #475569;
- font-size: 14px;
- line-height: 1;
- }
- :deep(.ant-radio-inner) {
- width: 16px;
- height: 16px;
- }
- }
- .newColumn-box {
- cursor: grab;
- user-select: none;
- &:active {
- cursor: grabbing;
- }
- &.is-dragging {
- opacity: 0.72;
- transform: scale(0.995);
- box-shadow: 0 8px 18px rgba(59, 130, 246, 0.12);
- }
- &.is-drag-over {
- color: #1d4ed8;
- border-color: #bfdbfe;
- background: linear-gradient(180deg, #eff6ff 0%, #eef4ff 100%);
- }
- .newColumn-text {
- flex: 1;
- min-width: 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- margin-right: 10px;
- }
- .del-icon {
- font-size: 16px;
- opacity: 1;
- line-height: 1;
- color: inherit;
- }
- }
- .right-input {
- margin-top: 10px;
- :deep(.ant-input) {
- height: 32px;
- line-height: 32px;
- font-size: 14px;
- }
- }
- }
- .dialog-footer {
- margin-top: 18px;
- padding-top: 16px;
- border-top: 1px solid #eef2f7;
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- :deep(.ant-btn) {
- min-width: 80px;
- height: 40px;
- padding: 0 20px;
- border-radius: 20px;
- border-width: 1px;
- font-size: 14px;
- font-weight: 500;
- box-shadow: none;
- }
- :deep(.ant-btn.add-col-confirm) {
- border-color: #3651d9;
- background: #3651d9;
- color: #fff;
- }
- :deep(.ant-btn.add-col-cancel) {
- border-color: #d1d5db;
- color: #475569;
- background: #fff;
- }
- }
- }
- }
- body.is-resizing-layout,
- body.is-resizing-layout * {
- user-select: none !important;
- -webkit-user-select: none !important;
- }
- .origin-field-popover {
- .ant-popover-inner {
- padding: 0;
- border-radius: 12px;
- border: 1px solid #e5e7eb;
- box-shadow: 0 16px 32px rgba(15, 23, 42, 0.12);
- }
- .ant-popover-inner-content {
- padding: 0;
- }
- .origin-pop {
- width: 430px;
- padding: 14px;
- .origin-pop-title {
- display: flex;
- align-items: center;
- gap: 12px;
- .small-title {
- font-size: 15px;
- font-weight: 600;
- white-space: nowrap;
- color: #111827;
- }
- }
- .origin-pop-list {
- margin-top: 12px;
- max-height: 260px;
- overflow: auto;
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 10px 12px;
- }
- .origin-tag-wrap {
- display: flex;
- align-items: center;
- gap: 6px;
- }
- .origin-tag {
- height: 34px;
- line-height: 32px;
- text-align: center;
- border-radius: 8px;
- background: #fff;
- border: 1px solid #e5e7eb;
- cursor: pointer;
- width: 139px;
- box-sizing: border-box;
- padding: 0 10px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #475569;
- &:hover {
- background: #f8fbff;
- border-color: #c8d7fb;
- color: #1d4ed8;
- }
- &.blank {
- background: #eef4ff;
- border-color: #c8d7fb;
- color: #1d4ed8;
- }
- &.disabled {
- color: #94a3b8;
- background: #f3f4f6;
- border-color: #e5e7eb;
- cursor: not-allowed;
- }
- &.active {
- color: #1d4ed8;
- border-color: #c8d7fb;
- background: #eef4ff;
- }
- }
- .replace {
- color: #3651d9;
- cursor: pointer;
- font-size: 14px;
- }
- .origin-pop-bottom {
- margin-top: 10px;
- text-align: right;
- color: #3651d9;
- cursor: pointer;
- }
- }
- }
- .select-template-modal {
- .ant-modal {
- width: calc(100vw - 32px) !important;
- max-width: 1080px;
- }
- .ant-modal-content {
- border-radius: 14px;
- overflow: hidden;
- border: 1px solid #e5e7eb;
- box-shadow: 0 18px 40px rgba(15, 23, 42, 0.14);
- }
- .ant-modal-header {
- padding: 10px 16px;
- border-bottom: 1px solid #eef2f7;
- background: @modal-header-bg-color;
- }
- .ant-modal-body {
- padding: 10px;
- background: #f8fafc;
- }
- .template-modal-layout {
- display: grid;
- grid-template-columns: 200px 460px 1fr;
- gap: 12px;
- height: 620px;
- }
- .modal-left,
- .modal-mid,
- .modal-right {
- border: 1px solid #e5e7eb;
- border-radius: 14px;
- background: #fff;
- min-height: 0;
- overflow: hidden;
- box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
- }
- .modal-panel-head {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 10px;
- .modal-panel-head__actions {
- display: flex;
- align-items: center;
- gap: 10px;
- flex-shrink: 0;
- }
- .panel-title {
- font-size: 15px;
- font-weight: 600;
- color: #111827;
- }
- .head-badge {
- flex-shrink: 0;
- padding: 4px 10px;
- border-radius: 999px;
- border: 1px solid #dbe7ff;
- background: #eef4ff;
- color: #3651d9;
- font-size: 12px;
- line-height: 1;
- font-weight: 600;
- }
- }
- .modal-left {
- padding: 10px;
- display: flex;
- flex-direction: column;
- .create-template-btn {
- height: 34px;
- border-radius: 10px;
- margin-bottom: 10px;
- }
- .cate-list {
- flex: 1;
- overflow: auto;
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .cate-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 12px;
- border-radius: 10px;
- border: 1px solid #edf2f7;
- background: #fff;
- cursor: pointer;
- transition: all 0.2s ease;
- .cate-name {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #334155;
- font-size: 13px;
- font-weight: 500;
- }
- .cate-count {
- flex-shrink: 0;
- min-width: 24px;
- height: 24px;
- padding: 0 8px;
- border-radius: 999px;
- background: #f8fafc;
- color: #64748b;
- font-size: 12px;
- line-height: 24px;
- text-align: center;
- }
- &:hover {
- border-color: #dbe7ff;
- background: #f8fbff;
- }
- &.active {
- border-color: #c8d7fb;
- background: #eef4ff;
- .cate-name,
- .cate-count {
- color: #3651d9;
- }
- .cate-count {
- background: #fff;
- }
- }
- }
- }
- .modal-mid {
- background: #fff;
- padding: 10px;
- display: flex;
- flex-direction: column;
- .tab-row {
- display: flex;
- gap: 8px;
- margin-bottom: 0;
- .tab {
- padding: 5px 12px;
- border: 1px solid #e5e7eb;
- border-radius: 999px;
- background: #fff;
- color: #475569;
- font-size: 12px;
- cursor: pointer;
- transition: all 0.2s ease;
- &.active {
- border-color: #c8d7fb;
- background: #eef4ff;
- color: #3651d9;
- }
- }
- }
- .filters {
- display: flex;
- gap: 10px;
- margin-bottom: 8px;
- padding: 8px 10px;
- border: 1px solid #e8edf5;
- border-radius: 12px;
- background: #f8fafc;
- .filter-item {
- display: flex;
- flex-direction: column;
- gap: 6px;
- min-width: 160px;
- &--grow {
- flex: 1;
- }
- }
- .filter-label {
- color: #64748b;
- font-size: 12px;
- font-weight: 600;
- line-height: 1;
- }
- .source-select {
- width: 160px;
- }
- }
- .list-summary {
- display: flex;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 10px;
- color: #6b7280;
- font-size: 12px;
- line-height: 1.6;
- }
- .tpl-list {
- flex: 1;
- overflow: auto;
- padding-right: 2px;
- }
- .tpl-item {
- min-height: 52px;
- padding: 7px 10px;
- border-radius: 12px;
- background: #fff;
- margin-bottom: 6px;
- display: flex;
- align-items: center;
- cursor: pointer;
- border: 1px solid #e5e7eb;
- position: relative;
- transition: all 0.2s ease;
- &:hover {
- border-color: #dbe7ff;
- background: #f8fbff;
- }
- &.active {
- background: #eef4ff;
- border-color: #c8d7fb;
- }
- .tpl-main {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 8px 12px;
- min-width: 0;
- flex: 1;
- }
- .name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #111827;
- font-size: 14px;
- font-weight: 600;
- line-height: 1.4;
- }
- .tpl-meta {
- display: flex;
- flex-wrap: wrap;
- gap: 4px 12px;
- min-width: 0;
- }
- .tpl-meta-text {
- color: #64748b;
- font-size: 12px;
- line-height: 1.4;
- white-space: nowrap;
- }
- .selected-mark {
- flex-shrink: 0;
- margin-left: 12px;
- padding: 3px 10px;
- border-radius: 999px;
- border: 1px solid #c8d7fb;
- background: #fff;
- color: #3651d9;
- font-size: 12px;
- line-height: 18px;
- font-weight: 600;
- }
- }
- .empty {
- text-align: center;
- color: #94a3b8;
- margin-top: 40px;
- }
- }
- .modal-right {
- padding: 10px;
- display: flex;
- flex-direction: column;
- .preview-overview {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 8px;
- margin-bottom: 6px;
- }
- .overview-card {
- padding: 8px 10px;
- border: 1px solid #e8edf5;
- border-radius: 12px;
- background: #f8fafc;
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
- .overview-label {
- color: #6b7280;
- font-size: 12px;
- line-height: 1;
- }
- .overview-value {
- color: #111827;
- font-size: 14px;
- font-weight: 600;
- line-height: 1.5;
- word-break: break-word;
- }
- .preview-meta {
- margin-bottom: 6px;
- padding: 8px 10px;
- border: 1px solid #e8edf5;
- border-radius: 12px;
- background: #fff;
- }
- .preview-meta-row {
- display: flex;
- gap: 12px;
- font-size: 13px;
- line-height: 1.6;
- margin-bottom: 0;
- &.preview-meta-row--headers {
- align-items: flex-start;
- }
- &:last-child {
- margin-bottom: 0;
- }
- }
- .meta-label {
- flex: 0 0 64px;
- color: #6b7280;
- }
- .meta-value {
- min-width: 0;
- flex: 1;
- color: #334155;
- word-break: break-word;
- &.meta-value--scroll {
- max-height: 88px;
- padding-right: 6px;
- overflow-y: auto;
- overflow-x: hidden;
- }
- }
- .preview-empty {
- margin-bottom: 8px;
- padding: 14px 12px;
- border: 1px dashed #dbe2f0;
- border-radius: 12px;
- background: #f8fbff;
- }
- .empty-title {
- color: #111827;
- font-size: 14px;
- font-weight: 600;
- }
- .empty-desc {
- margin-top: 6px;
- color: #6b7280;
- font-size: 12px;
- line-height: 1.7;
- }
- .preview-rule-title {
- margin-bottom: 4px;
- font-size: 14px;
- font-weight: 600;
- color: #111827;
- }
- .preview-rule-table {
- flex: 1;
- min-height: 0;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- overflow: auto;
- background: #fff;
- .rule-head,
- .rule-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- }
- .rule-head {
- background: #f3f4f6;
- font-weight: 600;
- color: #334155;
- }
- .rule-row {
- border-top: 1px solid #f1f5f9;
- color: #475569;
- }
- .rule-head > span,
- .rule-row > .rule-cell {
- padding: 8px 10px;
- font-size: 12px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .rule-cell {
- min-width: 0;
- &.rule-cell--target {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- }
- .rule-target-label {
- min-width: 0;
- display: inline-flex;
- align-items: center;
- color: #24324b;
- }
- .rule-required-mark {
- flex: 0 0 auto;
- margin-right: 4px;
- color: #ef4444;
- font-weight: 600;
- }
- .rule-target-text {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .rule-required-badge {
- flex: 0 0 auto;
- padding: 0 6px;
- border-radius: 999px;
- background: #fef2f2;
- color: #dc2626;
- font-size: 11px;
- line-height: 18px;
- font-weight: 500;
- }
- }
- .empty {
- color: #94a3b8;
- }
- .modal-actions {
- margin-top: auto;
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- padding-top: 8px;
- border-top: 1px solid #eef2f7;
- .ant-btn {
- min-width: 76px;
- height: 36px;
- padding: 0 18px;
- border-radius: 18px;
- border-width: 1px;
- font-size: 13px;
- font-weight: 500;
- box-shadow: none;
- }
- }
- }
- @media (max-width: 1260px) {
- .template-modal-layout {
- grid-template-columns: 220px minmax(0, 1fr);
- grid-template-rows: minmax(0, 1fr) 320px;
- height: auto;
- min-height: 640px;
- }
- .modal-right {
- grid-column: 1 / span 2;
- }
- .preview-rule-table {
- min-height: 180px;
- }
- }
- @media (max-width: 900px) {
- .template-modal-layout {
- grid-template-columns: 1fr;
- grid-template-rows: auto auto auto;
- height: auto;
- }
- .modal-right {
- grid-column: auto;
- }
- .filters,
- .list-summary {
- flex-direction: column;
- }
- .preview-overview {
- grid-template-columns: 1fr;
- }
- }
- }
- @keyframes cleaning-sheet-status-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- </style>
|