cleaning.vue 407 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203112041120511206112071120811209112101121111212112131121411215112161121711218112191122011221112221122311224112251122611227112281122911230112311123211233112341123511236112371123811239112401124111242112431124411245112461124711248112491125011251112521125311254112551125611257112581125911260112611126211263112641126511266112671126811269112701127111272112731127411275112761127711278112791128011281112821128311284112851128611287112881128911290112911129211293112941129511296112971129811299113001130111302113031130411305113061130711308113091131011311113121131311314113151131611317113181131911320113211132211323113241132511326113271132811329113301133111332113331133411335113361133711338113391134011341113421134311344113451134611347113481134911350113511135211353113541135511356113571135811359113601136111362113631136411365113661136711368113691137011371113721137311374113751137611377113781137911380113811138211383113841138511386113871138811389113901139111392113931139411395113961139711398113991140011401114021140311404114051140611407114081140911410114111141211413114141141511416114171141811419114201142111422114231142411425114261142711428114291143011431114321143311434114351143611437114381143911440114411144211443114441144511446114471144811449114501145111452114531145411455114561145711458114591146011461114621146311464114651146611467114681146911470114711147211473114741147511476114771147811479114801148111482114831148411485114861148711488114891149011491114921149311494114951149611497114981149911500115011150211503115041150511506115071150811509115101151111512115131151411515115161151711518115191152011521115221152311524115251152611527115281152911530115311153211533115341153511536115371153811539115401154111542115431154411545115461154711548115491155011551115521155311554115551155611557115581155911560115611156211563115641156511566115671156811569115701157111572115731157411575115761157711578115791158011581115821158311584115851158611587115881158911590115911159211593115941159511596115971159811599116001160111602116031160411605116061160711608116091161011611116121161311614116151161611617116181161911620116211162211623116241162511626116271162811629116301163111632116331163411635116361163711638116391164011641116421164311644116451164611647116481164911650116511165211653116541165511656116571165811659116601166111662116631166411665116661166711668116691167011671116721167311674116751167611677116781167911680116811168211683116841168511686116871168811689116901169111692116931169411695116961169711698116991170011701117021170311704117051170611707117081170911710117111171211713117141171511716117171171811719117201172111722117231172411725117261172711728117291173011731117321173311734117351173611737117381173911740117411174211743117441174511746117471174811749117501175111752117531175411755117561175711758117591176011761117621176311764117651176611767117681176911770117711177211773117741177511776117771177811779117801178111782117831178411785117861178711788117891179011791117921179311794117951179611797117981179911800118011180211803118041180511806118071180811809118101181111812118131181411815118161181711818118191182011821118221182311824118251182611827118281182911830118311183211833118341183511836118371183811839118401184111842118431184411845118461184711848118491185011851118521185311854118551185611857118581185911860118611186211863118641186511866118671186811869118701187111872118731187411875118761187711878118791188011881118821188311884118851188611887118881188911890118911189211893118941189511896118971189811899119001190111902119031190411905119061190711908119091191011911119121191311914119151191611917119181191911920119211192211923119241192511926119271192811929119301193111932119331193411935119361193711938119391194011941119421194311944119451194611947119481194911950119511195211953119541195511956119571195811959119601196111962119631196411965119661196711968119691197011971119721197311974119751197611977119781197911980119811198211983119841198511986119871198811989119901199111992119931199411995119961199711998119991200012001120021200312004120051200612007120081200912010120111201212013120141201512016120171201812019120201202112022120231202412025120261202712028120291203012031120321203312034120351203612037120381203912040120411204212043120441204512046120471204812049120501205112052120531205412055120561205712058120591206012061120621206312064120651206612067120681206912070120711207212073120741207512076120771207812079120801208112082120831208412085120861208712088120891209012091120921209312094120951209612097120981209912100121011210212103121041210512106121071210812109121101211112112
  1. <template>
  2. <div :class="prefixCls" class="case-data-cleaning-page">
  3. <div class="page-header">
  4. <div class="page-header-main">
  5. <div class="page-title-area">
  6. <Button type="text" class="back-btn" @click="handleBack">←</Button>
  7. <div class="page-title-block">
  8. <div class="page-title">案件数据清洗</div>
  9. </div>
  10. </div>
  11. <div class="page-header-actions">
  12. <Button @click="toggleLeft">{{ leftCollapsed ? '展开左侧' : '收起左侧' }}</Button>
  13. <Button type="primary" :loading="importingData" @click="handleImportData">导入数据</Button>
  14. </div>
  15. </div>
  16. </div>
  17. <div class="main-layout" :style="{ gridTemplateColumns: layoutTemplateColumns }">
  18. <aside v-show="!leftCollapsed" class="left-panel">
  19. <div class="tree-header">
  20. <span class="label">文件分类</span>
  21. <div class="actions">
  22. <Button size="small" @click="sortMode = sortMode === 'asc' ? '' : 'asc'">升序</Button>
  23. <Button size="small" @click="sortMode = sortMode === 'desc' ? '' : 'desc'">降序</Button>
  24. </div>
  25. </div>
  26. <div class="tree-wrap">
  27. <BasicTree
  28. class="cleaning-file-tree"
  29. :tree-data="treeData"
  30. :tree-data-simple-mode="false"
  31. :field-names="{ key: 'key', title: 'title', children: 'children' }"
  32. :default-expand-all="true"
  33. v-model:selectedKeys="selectedKeys"
  34. @select="handleSelect"
  35. >
  36. <template #title="node">
  37. <div v-if="isTreeFileNode(node.raw)" class="tree-file-row" :title="getTreeFileName(node.raw)">
  38. <span class="tree-file-name">{{ getTreeFileName(node.raw) }}</span>
  39. </div>
  40. <div
  41. v-else
  42. class="tree-sheet-row"
  43. :class="{
  44. 'is-success': getSheetNodeVisualState(node.raw) === 'success',
  45. 'is-fail': getSheetNodeVisualState(node.raw) === 'fail',
  46. 'is-analyzing': getSheetNodeVisualState(node.raw) === 'analyzing',
  47. }"
  48. :title="getTreeSheetTitle(node.raw)"
  49. >
  50. <span class="tree-sheet-status">
  51. <span
  52. v-if="getSheetNodeVisualState(node.raw) === 'analyzing'"
  53. class="tree-sheet-status-spinner"
  54. ></span>
  55. <template v-else>
  56. {{ getSheetNodeVisualState(node.raw) === 'success' ? '✓' : '✕' }}
  57. </template>
  58. </span>
  59. <span class="tree-sheet-name">{{ getTreeSheetName(node.raw) }}</span>
  60. </div>
  61. </template>
  62. </BasicTree>
  63. </div>
  64. <div class="legend">
  65. <div v-for="item in iconTips" :key="item.key" class="legend-item">
  66. <span class="legend-icon" :class="`is-${item.key}`">
  67. <span v-if="item.key === 'matched' || item.key === 'unmatched'" class="legend-dot"></span>
  68. <template v-else>{{ item.icon }}</template>
  69. </span>
  70. <span class="legend-text">{{ item.label }}</span>
  71. </div>
  72. </div>
  73. </aside>
  74. <div v-show="!leftCollapsed" class="panel-resizer" @mousedown="startResize('left', $event)"></div>
  75. <section class="middle-panel">
  76. <div class="template-match-bar" :class="{ 'is-empty': !hasTemplateSelected }">
  77. <div class="template-match-main">
  78. <span class="template-match-icon"></span>
  79. <div class="template-match-copy">
  80. <div class="template-match-label">匹配模板</div>
  81. <div class="template-match-name">
  82. {{ currentTemplateDisplayName }}
  83. <span v-if="!hasTemplateSelected" class="template-match-badge is-pending">待配置</span>
  84. <span v-else-if="templateDetailFromMock" class="template-match-badge">示例</span>
  85. </div>
  86. </div>
  87. </div>
  88. <Button type="link" class="template-select-trigger" @click="openTemplateModal">选择模板</Button>
  89. </div>
  90. <template v-if="hasTemplateSelected">
  91. <div class="middle-panel-body">
  92. <div class="mapping-table-card">
  93. <div class="mapping-card-head">
  94. <div class="mapping-card-title">字段映射</div>
  95. <div class="mapping-card-actions">
  96. <Button size="small" @click="() => autoMatchFields()">智能预匹配</Button>
  97. <Button size="small" @click="clearFieldMapping">清空映射</Button>
  98. </div>
  99. </div>
  100. <div class="mapping-table">
  101. <div class="mapping-head">
  102. <span>序号</span>
  103. <span>状态</span>
  104. <span>目标字段</span>
  105. <span>原始字段</span>
  106. </div>
  107. <div
  108. class="mapping-row"
  109. :class="{ 'is-active': activeMappingTargetKey === item.key }"
  110. v-for="(item, index) in sortedMappingTargetFields"
  111. :key="item.key"
  112. @click="handleMappingRowSelect(item.key)"
  113. >
  114. <span>{{ index + 1 }}</span>
  115. <span class="status-cell">
  116. <span class="mapping-status-group">
  117. <span class="mapping-status-badge" :class="getMappingStatusClass(item)">
  118. <span v-if="getMappingStatusClass(item) !== 'is-optional'" class="mapping-status-dot"></span>
  119. </span>
  120. <span
  121. v-if="hasTargetFormatError(item.key)"
  122. class="mapping-status-error"
  123. :title="getTargetFormatErrorTitle(item.key) || undefined"
  124. >
  125. !
  126. </span>
  127. </span>
  128. </span>
  129. <span class="target-field-cell">
  130. <span v-if="isMappingTargetRequired(item)" class="required">*</span>
  131. {{ item.label }}
  132. </span>
  133. <span class="origin-field-cell">
  134. <Popover
  135. trigger="click"
  136. placement="bottomRight"
  137. overlayClassName="origin-field-popover"
  138. :open="!isTargetHandledByRecognitionRule(item.key) && mappingPopoverOpenKey === item.key"
  139. @openChange="(open) => handleMappingPopoverOpen(item.key, open)"
  140. >
  141. <template #content>
  142. <div class="origin-pop">
  143. <div class="origin-pop-title">
  144. <span class="small-title">原始字段选择</span>
  145. <Input
  146. v-model:value="originFilterKeyword"
  147. size="small"
  148. placeholder="请输入搜索的字段名称"
  149. allow-clear
  150. />
  151. </div>
  152. <div
  153. class="origin-pop-list"
  154. :data-origin-list-target-key="item.key"
  155. @mouseleave="handleOriginOptionHoverLeave(item.key)"
  156. >
  157. <div
  158. class="origin-tag blank"
  159. @mouseenter="handleOriginOptionHoverLeave(item.key)"
  160. @click="setFieldBlank(item.key)"
  161. >
  162. 置空
  163. </div>
  164. <div
  165. v-for="opt in filteredOriginalOptions"
  166. :key="opt.value"
  167. class="origin-tag-wrap"
  168. @mouseenter="handleOriginOptionHover(item.key, opt.value)"
  169. >
  170. <div
  171. class="origin-tag"
  172. :data-origin-target-key="item.key"
  173. :data-origin-option-value="opt.value"
  174. :class="{
  175. disabled: isFieldTakenByOther(opt.value, item.key),
  176. active:
  177. getDisplayedMappedHeader(item.key) === opt.value ||
  178. isOriginOptionHovering(item.key, opt.value),
  179. }"
  180. @click="selectOriginalField(item.key, opt.value)"
  181. >
  182. {{ opt.label }}
  183. </div>
  184. <span
  185. v-if="isFieldTakenByOther(opt.value, item.key)"
  186. class="replace"
  187. @click="replaceOriginalField(item.key, opt.value)"
  188. >替换</span
  189. >
  190. </div>
  191. </div>
  192. <div class="origin-pop-bottom">+添加自定义表头</div>
  193. </div>
  194. </template>
  195. <div
  196. class="mapping-field-trigger"
  197. :class="{ 'is-disabled': isTargetHandledByRecognitionRule(item.key) }"
  198. @click="handleMappingTriggerClick(item.key)"
  199. >
  200. <span
  201. class="mapping-field-value"
  202. :class="{
  203. placeholder:
  204. !getDisplayedMappedHeader(item.key) && !isTargetHandledByRecognitionRule(item.key),
  205. }"
  206. >
  207. {{
  208. isTargetHandledByRecognitionRule(item.key)
  209. ? ''
  210. : getDisplayedMappedHeader(item.key) || '请选择原始字段'
  211. }}
  212. </span>
  213. <span v-if="!isTargetHandledByRecognitionRule(item.key)" class="mapping-field-arrow">▾</span>
  214. </div>
  215. </Popover>
  216. </span>
  217. </div>
  218. <div v-if="!fixedTargetFields.length" class="mapping-empty">当前模板暂无可映射字段</div>
  219. </div>
  220. </div>
  221. <div
  222. class="template-foot-panel"
  223. :class="{
  224. 'is-collapsed': showRecognitionPanel && !recognitionPanelExpanded,
  225. 'is-compact': !showRecognitionPanel,
  226. }"
  227. >
  228. <div v-if="showRecognitionPanel" class="recognition-panel">
  229. <div class="recognition-panel-head">
  230. <span class="recognition-title">识别规则设置</span>
  231. <span
  232. class="recognition-toggle"
  233. :class="{ expanded: recognitionPanelExpanded }"
  234. @click="recognitionPanelExpanded = !recognitionPanelExpanded"
  235. >
  236. {{ recognitionPanelExpanded ? '收起' : '展开' }}
  237. <component
  238. :is="recognitionPanelExpanded ? DownOutlined : UpOutlined"
  239. class="recognition-toggle-icon"
  240. />
  241. </span>
  242. </div>
  243. <div v-if="recognitionPanelExpanded" class="recognition-panel-body">
  244. <div class="recognition-tabs">
  245. <span
  246. v-for="item in recognitionRuleTabs"
  247. :key="item"
  248. class="recognition-tab"
  249. :class="{ active: activeRecognitionRule === item }"
  250. @click="activeRecognitionRule = item"
  251. >
  252. {{ item }}
  253. </span>
  254. </div>
  255. <div v-if="activeRecognitionRule === '基本信息配置'" class="recognition-basic-shell">
  256. <div class="recognition-basic-preview">
  257. <div class="recognition-basic-preview__head">
  258. <span class="recognition-basic-preview__title">表头上方内容</span>
  259. <span class="recognition-basic-preview__meta">{{ recognitionHeaderPreviewMeta }}</span>
  260. </div>
  261. <div class="recognition-basic-preview__content">{{ recognitionHeaderPreviewText }}</div>
  262. </div>
  263. <div v-if="showRecognitionBasicInfoPanel" class="recognition-basic-panel">
  264. <div class="recognition-basic-panel__head">
  265. <div class="recognition-basic-panel__summary">
  266. <div class="recognition-basic-panel__title-wrap">
  267. <span class="recognition-basic-panel__title">合并表头配置内容</span>
  268. <span class="recognition-basic-panel__count">{{ recognitionBasicInfoRows.length }} 项</span>
  269. </div>
  270. <div class="recognition-basic-panel__desc">
  271. 跟随模板中的合并表逻辑,为表头上方内容补充原始匹配项、目标字段与截止标志,目标字段保持模板默认映射。
  272. </div>
  273. </div>
  274. <div class="recognition-basic-panel__actions">
  275. <Button
  276. type="link"
  277. class="recognition-basic-panel__toggle-btn"
  278. @click.stop="recognitionBasicInfoExpanded = !recognitionBasicInfoExpanded"
  279. >
  280. {{ recognitionBasicInfoExpanded ? '收起' : '展开' }}
  281. </Button>
  282. </div>
  283. </div>
  284. <template v-if="recognitionBasicInfoExpanded">
  285. <div v-if="recognitionBasicInfoRows.length" class="recognition-basic-list">
  286. <div
  287. v-for="(item, index) in recognitionBasicInfoRows"
  288. :key="item.key"
  289. class="recognition-basic-item"
  290. >
  291. <div class="recognition-basic-item__index">{{ String(index + 1).padStart(2, '0') }}</div>
  292. <div class="recognition-basic-item__content">
  293. <div class="recognition-basic-field">
  294. <span class="recognition-basic-field__label">原始表头匹配项</span>
  295. <Input v-model:value="item.headerMatch" placeholder="请输入匹配项" />
  296. </div>
  297. <div class="recognition-basic-field">
  298. <span class="recognition-basic-field__label">目标字段</span>
  299. <Tooltip :title="item.targetField || undefined" placement="topLeft">
  300. <div
  301. class="recognition-basic-field__readonly"
  302. :class="{ 'is-empty': !item.targetField }"
  303. >
  304. {{ item.targetField || '未配置目标字段' }}
  305. </div>
  306. </Tooltip>
  307. </div>
  308. <div class="recognition-basic-field">
  309. <span class="recognition-basic-field__label">内容获取截止标志</span>
  310. <Input v-model:value="item.endFlag" placeholder="请输入截止标志" />
  311. </div>
  312. </div>
  313. </div>
  314. </div>
  315. <div v-else class="recognition-basic-empty">
  316. <div class="recognition-basic-empty__title">暂无合并配置</div>
  317. <div class="recognition-basic-empty__desc">
  318. 当前模板未解析到可展示的合并规则,请检查默认提取规则配置。
  319. </div>
  320. </div>
  321. </template>
  322. </div>
  323. <div v-else class="recognition-basic-empty-shell">
  324. <div class="recognition-basic-empty-shell__title">当前模板暂无合并表配置</div>
  325. <div class="recognition-basic-empty-shell__desc">
  326. 当前工作表会沿用模板默认合并表配置;若需调整,请先在模板库中维护对应的默认规则。
  327. </div>
  328. </div>
  329. </div>
  330. <div v-else-if="activeRecognitionRule === '正负标识'" class="recognition-zf-shell">
  331. <div class="recognition-zf-card">
  332. <div class="recognition-zf-card__main">
  333. <div class="recognition-zf-card__title">交易金额正负标识</div>
  334. <div class="recognition-zf-card__desc">
  335. 开启后,系统会依据交易金额的正负值识别收入/支出,借贷标志字段无需再单独映射原始列。
  336. </div>
  337. </div>
  338. <Switch v-model:checked="recognitionAmountZfEnabled" @change="handleRecognitionAmountZfChange" />
  339. </div>
  340. </div>
  341. <div
  342. v-else-if="activeRecognitionRule === '借贷标志' || activeRecognitionRule === '通信类别'"
  343. class="recognition-direction-shell"
  344. >
  345. <div class="recognition-direction-card-list">
  346. <div
  347. v-for="field in activeRecognitionDirectionFields"
  348. :key="field.key"
  349. class="recognition-direction-card"
  350. >
  351. <div class="recognition-direction-card__head">
  352. <span class="recognition-direction-card__title">{{ field.label }}</span>
  353. <span class="recognition-direction-card__tag">{{ field.entries.length }} 项</span>
  354. </div>
  355. <div class="recognition-direction-list">
  356. <div
  357. v-for="directionItem in field.entries"
  358. :key="directionItem.key"
  359. class="recognition-direction-row"
  360. >
  361. <span class="recognition-direction-label">{{ directionItem.label }}</span>
  362. <Tooltip :title="directionItem.value || undefined" placement="topLeft">
  363. <Input v-model:value="directionItem.value" placeholder="请输入识别关键词" />
  364. </Tooltip>
  365. </div>
  366. </div>
  367. </div>
  368. </div>
  369. <div class="recognition-direction-actions">
  370. <Button @click="handleClearRecognitionDirection">清空规则</Button>
  371. <Button type="primary" @click="handleSaveRecognitionDirection">保存</Button>
  372. </div>
  373. </div>
  374. <div v-else class="recognition-rule-placeholder">
  375. <div class="recognition-rule-placeholder__title">{{ activeRecognitionRule }}</div>
  376. <div class="recognition-rule-placeholder__desc">
  377. 当前规则仍沿用已配置的模板字段与右侧清洗逻辑,后续可继续补充专项识别参数。
  378. </div>
  379. </div>
  380. </div>
  381. </div>
  382. <div class="child-template-panel">
  383. <div class="child-template-tip">
  384. 子模板输入名称,导入数据会自动保存。下次上传同样的数据子模板会自动识别。
  385. </div>
  386. <div class="child-template-form">
  387. <span class="child-template-label">创建子模板:</span>
  388. <Input
  389. v-model:value="childTemplateName"
  390. placeholder="请输入子模板名称"
  391. @blur="handleChildTemplateNameBlur"
  392. />
  393. </div>
  394. </div>
  395. </div>
  396. </div>
  397. </template>
  398. <div v-else class="middle-empty-shell">
  399. <div class="middle-empty-stage">
  400. <div class="middle-empty-stage-head">
  401. <span class="middle-empty-sheet">当前工作表:{{ activeSheetDisplayName }}</span>
  402. </div>
  403. <div class="middle-empty-stage-main">
  404. <div class="middle-empty-visual" aria-hidden="true">
  405. <span class="middle-empty-visual__halo is-large"></span>
  406. <span class="middle-empty-visual__halo is-small"></span>
  407. <span class="middle-empty-visual__card"></span>
  408. <span class="middle-empty-visual__card is-back"></span>
  409. <span class="middle-empty-visual__dot"></span>
  410. </div>
  411. <div class="middle-empty-actions">
  412. <Button type="primary" @click="openTemplateModal">选择模板</Button>
  413. <span class="middle-empty-hint">支持从模板库实时加载模板,并同步初始化字段映射区。</span>
  414. </div>
  415. </div>
  416. </div>
  417. <div class="middle-empty-highlights">
  418. <div class="middle-empty-card">
  419. <span class="middle-empty-card__index">01</span>
  420. <div class="middle-empty-card__title">锁定字段结构</div>
  421. <div class="middle-empty-card__desc">先明确目标字段与必填项,再进入映射配置,减少重复调整。</div>
  422. </div>
  423. <div class="middle-empty-card">
  424. <span class="middle-empty-card__index">02</span>
  425. <div class="middle-empty-card__title">自动回填规则</div>
  426. <div class="middle-empty-card__desc">命中模板后可直接带出字段关系,并用当前表头继续补齐。</div>
  427. </div>
  428. <div class="middle-empty-card">
  429. <span class="middle-empty-card__index">03</span>
  430. <div class="middle-empty-card__title">沉淀子模板</div>
  431. <div class="middle-empty-card__desc">完成映射后可保存识别规则,方便同类文件后续自动识别。</div>
  432. </div>
  433. </div>
  434. </div>
  435. </section>
  436. <div class="panel-resizer" @mousedown="startResize('middle', $event)"></div>
  437. <section class="right-panel">
  438. <div class="panel-header">
  439. <div class="panel-title-wrap">
  440. <div class="panel-title">数据清洗区</div>
  441. </div>
  442. </div>
  443. <div class="toolbar toolbar--column-actions">
  444. <div class="toolbar-head">
  445. <span class="toolbar-label">列操作</span>
  446. </div>
  447. <div class="toolbar-actions">
  448. <Button size="small" type="primary" @click="openAddColumn">新增列</Button>
  449. <Button size="small" @click="openSplitColumn">拆分列</Button>
  450. <Button size="small" @click="openEditColumn">编辑列</Button>
  451. <Button size="small" @click="openDeleteColumn">删除列</Button>
  452. </div>
  453. <div class="toolbar-meta toolbar-meta--header-row">
  454. <span class="meta-label">表头行</span>
  455. <Input
  456. v-model:value="headerRowInput"
  457. size="small"
  458. class="meta-input"
  459. placeholder="输入行号"
  460. @pressEnter="applyHeaderRowInput"
  461. @blur="applyHeaderRowInput"
  462. />
  463. </div>
  464. </div>
  465. <div class="right-panel-body">
  466. <div ref="tableMockRef" class="table-mock">
  467. <div class="mock-head" :style="{ gridTemplateColumns: mockGridTemplate }">
  468. <span class="row-index-cell row-index-head">表头行</span>
  469. <span
  470. v-for="(header, colIndex) in rightTableHeaders"
  471. :key="header"
  472. :data-preview-col="colIndex"
  473. :class="{
  474. 'is-added': addedColumns.includes(header),
  475. active: selectedAddedColumn === header,
  476. 'is-current-col': currentColumn === colIndex,
  477. 'is-first-col': colIndex === 0,
  478. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  479. }"
  480. @click="onClickHeader(header)"
  481. >
  482. <span>{{ header }}</span>
  483. <span
  484. v-if="getColumnRuleBadgeState(colIndex)"
  485. class="rule-badge"
  486. :class="`is-${getColumnRuleBadgeState(colIndex)}`"
  487. :title="getColumnRuleBadgeTitle(colIndex)"
  488. ></span>
  489. </span>
  490. </div>
  491. <div class="mock-match-row" :style="{ gridTemplateColumns: mockGridTemplate }">
  492. <span class="row-index-cell match-row-index">目标字段</span>
  493. <span
  494. v-for="(header, colIndex) in rightTableHeaders"
  495. :key="`mapping-${header}`"
  496. :data-preview-col="colIndex"
  497. :class="{
  498. 'is-current-col': currentColumn === colIndex,
  499. 'is-first-col': colIndex === 0,
  500. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  501. 'is-empty': !getMatchedTargetFieldText(header),
  502. }"
  503. @click="onClickHeader(header)"
  504. >
  505. <span class="match-field-content">
  506. <span class="match-field-text">{{ getMatchedTargetFieldText(header) || '-' }}</span>
  507. <span class="match-status-group">
  508. <span
  509. class="match-status-badge"
  510. :class="getMatchedHeaderStatusClass(header)"
  511. :title="getMatchedHeaderStatusTitle(header) || undefined"
  512. >
  513. {{ getMatchedHeaderStatusText(header) }}
  514. </span>
  515. <span
  516. v-if="hasHeaderFormatError(header)"
  517. class="match-status-badge is-formatError"
  518. :title="getHeaderFormatErrorTitle(header) || undefined"
  519. >
  520. !
  521. </span>
  522. </span>
  523. </span>
  524. </span>
  525. </div>
  526. <div
  527. class="mock-row"
  528. :style="{ gridTemplateColumns: mockGridTemplate }"
  529. :class="{
  530. 'is-ignored-row': ignoredRowsSet.has(rowIndex + 1),
  531. 'is-current-row': selectedRowIndex === rowIndex,
  532. 'is-header-row': headerRowIndex === rowIndex,
  533. }"
  534. v-for="(row, rowIndex) in tablePreviewRows"
  535. :key="`row-${rowIndex}`"
  536. >
  537. <span class="row-index-cell" @click="selectedRowIndex = rowIndex">
  538. {{ rowIndex + 1 }}
  539. </span>
  540. <span
  541. v-for="(header, colIndex) in rightTableHeaders"
  542. :key="`${header}-${rowIndex}`"
  543. :data-preview-col="colIndex"
  544. :class="{
  545. 'is-current-col': currentColumn === colIndex,
  546. 'is-first-col': colIndex === 0,
  547. 'is-last-col': colIndex === rightTableHeaders.length - 1,
  548. }"
  549. @click="onClickCell(rowIndex, header, colIndex)"
  550. >
  551. {{ row[header] || '' }}
  552. </span>
  553. </div>
  554. </div>
  555. <div class="cleaning-rule-panel" :class="{ 'is-collapsed': !cleaningRulePanelExpanded }">
  556. <div class="rule-title-row">
  557. <div class="rule-tabs">
  558. <span
  559. v-for="(tab, idx) in rulesList"
  560. :key="tab.name"
  561. class="rule-tab"
  562. :class="{ active: activeRules === idx }"
  563. @click="rulesListCheck(idx)"
  564. >
  565. {{ tab.name }}
  566. </span>
  567. </div>
  568. <div class="rule-title-meta">
  569. <span class="current-col">当前选中列:{{ currentColumnTitle || '-' }}</span>
  570. <span
  571. class="rule-toggle"
  572. :class="{ expanded: cleaningRulePanelExpanded }"
  573. @click="cleaningRulePanelExpanded = !cleaningRulePanelExpanded"
  574. >
  575. {{ cleaningRulePanelExpanded ? '收起' : '展开' }}
  576. <component :is="cleaningRulePanelExpanded ? DownOutlined : UpOutlined" class="rule-toggle-icon" />
  577. </span>
  578. </div>
  579. </div>
  580. <div v-show="cleaningRulePanelExpanded" class="rule-content">
  581. <div class="rule-col">
  582. <div class="col-title">清洗规则</div>
  583. <div class="col-list">
  584. <div
  585. v-for="(item, idx) in visibleCleaningRules"
  586. :key="`${item.key}-${idx}`"
  587. class="col-item"
  588. :class="{ active: item.active }"
  589. @click="rulesListClick(item.key)"
  590. @dblclick="dbRulesListClick(item.key)"
  591. >
  592. {{ item.name }}
  593. </div>
  594. </div>
  595. </div>
  596. <div class="rule-op">
  597. <Button size="small" @click="rulesListRight">&gt;</Button>
  598. <Button size="small" @click="rulesListLeft">&lt;</Button>
  599. </div>
  600. <div class="rule-col">
  601. <div class="col-title between">
  602. <span>已选规则</span>
  603. <span class="clear-btn" @click="selectRulesDel">全部删除</span>
  604. </div>
  605. <div class="col-list">
  606. <div
  607. v-for="(item, idx) in currentSelectedRules"
  608. :key="`${item.key}-${idx}`"
  609. class="col-item"
  610. :class="{ active: item.active }"
  611. @click="selectRulesClick(idx)"
  612. >
  613. <span class="col-item-label">{{ item.name }}</span>
  614. <span class="col-item-status" :class="{ 'is-enabled': item.enabled, 'is-disabled': !item.enabled }">
  615. {{ item.enabled ? '已启用' : '未启用' }}
  616. </span>
  617. </div>
  618. </div>
  619. </div>
  620. <div class="rule-config">
  621. <div class="col-title">{{
  622. currentActiveSelectedRule || !currentRuleExample ? '规则配置' : '示例'
  623. }}</div>
  624. <div class="config-placeholder">
  625. <template v-if="currentActiveSelectedRule">
  626. <div class="config-rule-shell">
  627. <div class="config-rule-head">
  628. <div class="config-rule-title-row">
  629. <div class="config-rule-copy">
  630. <div class="config-rule-title">
  631. {{ currentRuleTitle || ruleNameMap[currentActiveSelectedRule.key] }}
  632. </div>
  633. </div>
  634. <div class="config-rule-meta">
  635. <span class="config-rule-state" :class="currentRuleEnabledClass">
  636. {{ currentRuleEnabledLabel }}
  637. </span>
  638. <span class="config-rule-tag" :class="`is-${currentRuleScopeType}`">
  639. {{ currentRuleScopeLabel }}
  640. </span>
  641. <Button
  642. v-if="currentActiveSelectedRule.enabled"
  643. size="small"
  644. class="config-rule-toggle-btn"
  645. @click="toggleCurrentRuleEnabled(false)"
  646. >
  647. 停用规则
  648. </Button>
  649. </div>
  650. </div>
  651. </div>
  652. <template v-if="currentActiveSelectedRule.key === 'ignoreLine'">
  653. <div class="config-section">
  654. <div class="config-section-title">参数配置</div>
  655. <div class="config-field">
  656. <span class="config-field-label">忽略行号</span>
  657. <Input v-model:value="omitLineInput" placeholder="例如:2,5,8" />
  658. <div class="config-hint">
  659. 按 1 开始计数,多个行号使用英文逗号分隔,预览结果和左侧结构会同步忽略这些行。
  660. </div>
  661. </div>
  662. <div class="config-actions">
  663. <Button type="primary" size="small" @click="applyIgnoreLineRule">应用规则</Button>
  664. </div>
  665. </div>
  666. <div class="config-section is-example">
  667. <div class="config-section-title">规则示例</div>
  668. <div class="config-example-list">
  669. <div class="config-example-row">
  670. <span>输入</span>
  671. <code>2,5,8</code>
  672. </div>
  673. <div class="config-example-row">
  674. <span>效果</span>
  675. <span>第 2、5、8 行不会参与后续清洗与导入</span>
  676. </div>
  677. </div>
  678. </div>
  679. </template>
  680. <template v-else-if="currentActiveSelectedRule.key === 'dateConfig'">
  681. <div class="config-section">
  682. <div class="config-section-title">参数配置</div>
  683. <div class="config-field">
  684. <span class="config-field-label">输出类型</span>
  685. <Select v-model:value="dateConfigType" :options="dateTypeOptions" />
  686. <div class="config-hint">
  687. 根据源值自动识别常见日期格式,并统一转换为标准日期文本输出。
  688. </div>
  689. </div>
  690. <div class="config-actions">
  691. <Button type="primary" size="small" @click="applyDateConfigRule">应用规则</Button>
  692. </div>
  693. </div>
  694. <div class="config-section is-example">
  695. <div class="config-section-title">规则示例</div>
  696. <div class="config-example-list">
  697. <div class="config-example-row">
  698. <span>原始值</span>
  699. <code>20260324153020</code>
  700. </div>
  701. <div class="config-example-row">
  702. <span>日期时间</span>
  703. <code>2026-03-24 15:30:20</code>
  704. </div>
  705. <div class="config-example-row">
  706. <span>仅日期</span>
  707. <code>2026-03-24</code>
  708. </div>
  709. </div>
  710. </div>
  711. </template>
  712. <template v-else-if="currentActiveSelectedRule.key === 'extractData'">
  713. <div class="config-section">
  714. <div class="config-section-title">参数配置</div>
  715. <Radio.Group v-model:value="extractDataMode" class="config-extract-data-group">
  716. <div class="config-extract-data-item">
  717. <Radio value="before" @change="handleExtractDataModeChange('before')">提取前</Radio>
  718. <Input
  719. v-model:value="extractDataBefore"
  720. :disabled="extractDataMode !== 'before'"
  721. placeholder="例如:6"
  722. />
  723. <span class="config-extract-data-unit">位</span>
  724. </div>
  725. <div class="config-extract-data-item">
  726. <Radio value="after" @change="handleExtractDataModeChange('after')">提取后</Radio>
  727. <Input
  728. v-model:value="extractDataAfter"
  729. :disabled="extractDataMode !== 'after'"
  730. placeholder="例如:4"
  731. />
  732. <span class="config-extract-data-unit">位</span>
  733. </div>
  734. </Radio.Group>
  735. <div class="config-hint">提取前与提取后互斥,只会应用当前选中的提取方向。</div>
  736. <div class="config-actions">
  737. <Button type="primary" size="small" @click="applyExtractDataRule">应用规则</Button>
  738. </div>
  739. </div>
  740. <div class="config-section is-example">
  741. <div class="config-section-title">规则示例</div>
  742. <div class="config-example-list">
  743. <div class="config-example-row">
  744. <span>原始值</span>
  745. <code>13800138000</code>
  746. </div>
  747. <div class="config-example-row">
  748. <span>前 3 位</span>
  749. <code>138</code>
  750. </div>
  751. <div class="config-example-row">
  752. <span>后 4 位</span>
  753. <code>8000</code>
  754. </div>
  755. </div>
  756. </div>
  757. </template>
  758. <template v-else-if="currentActiveSelectedRule.key === 'extractSegment'">
  759. <div class="config-section">
  760. <div class="config-section-title">参数配置</div>
  761. <div class="config-form-grid">
  762. <div class="config-field">
  763. <span class="config-field-label">分隔符</span>
  764. <Input v-model:value="extractSegmentValue" placeholder="例如:-" />
  765. </div>
  766. <div class="config-field">
  767. <span class="config-field-label">提取段位</span>
  768. <InputNumber v-model:value="extractSegmentRow" :min="1" :precision="0" />
  769. </div>
  770. </div>
  771. <div class="config-hint">按分隔符拆分后,从左到右提取指定段位内容。</div>
  772. <div class="config-actions">
  773. <Button type="primary" size="small" @click="applyExtractSegmentRule">应用规则</Button>
  774. </div>
  775. </div>
  776. <div class="config-section is-example">
  777. <div class="config-section-title">规则示例</div>
  778. <div class="config-example-list">
  779. <div class="config-example-row">
  780. <span>原始值</span>
  781. <code>四川省-成都市-高新区</code>
  782. </div>
  783. <div class="config-example-row">
  784. <span>分隔符</span>
  785. <code>-</code>
  786. </div>
  787. <div class="config-example-row">
  788. <span>第 2 段</span>
  789. <code>成都市</code>
  790. </div>
  791. </div>
  792. </div>
  793. </template>
  794. <template v-else-if="currentActiveSelectedRule.key === 'timestampConversion'">
  795. <div class="config-section">
  796. <div class="config-section-title">规则说明</div>
  797. <div class="config-desc-list">
  798. <div class="config-desc-item">自动识别 10 位或 13 位时间戳,并转换为标准时间文本。</div>
  799. <div class="config-desc-item">适用于毫秒时间戳、秒时间戳以及常见纯数字时间字段。</div>
  800. </div>
  801. <div class="config-actions">
  802. <Button type="primary" size="small" @click="applyTimestampConversionRule">确认启用</Button>
  803. </div>
  804. </div>
  805. <div class="config-section is-example">
  806. <div class="config-section-title">规则示例</div>
  807. <div class="config-example-list">
  808. <div class="config-example-row">
  809. <span>时间戳</span>
  810. <code>1711209600</code>
  811. </div>
  812. <div class="config-example-row">
  813. <span>转换后</span>
  814. <code>2024-03-24 00:00:00</code>
  815. </div>
  816. </div>
  817. </div>
  818. </template>
  819. <template v-else-if="currentActiveSelectedRule.key === 'removeSpecialCharacters'">
  820. <div class="config-section">
  821. <div class="config-section-title">参数配置</div>
  822. <div class="config-check-list">
  823. <Checkbox v-model:checked="removeSpaceChecked">移除空格字符</Checkbox>
  824. <Checkbox v-model:checked="removeTabChecked">移除换行字符(tab)</Checkbox>
  825. </div>
  826. <div class="config-field">
  827. <span class="config-field-label">自定义移除字符</span>
  828. <div class="config-check-inline">
  829. <Checkbox v-model:checked="removeCustomChecked">启用</Checkbox>
  830. <Input
  831. v-model:value="removeCustomText"
  832. :disabled="!removeCustomChecked"
  833. placeholder="例如:-"
  834. />
  835. </div>
  836. </div>
  837. <div class="config-actions">
  838. <Button type="primary" size="small" @click="applyRemoveSpecialRule">应用规则</Button>
  839. </div>
  840. </div>
  841. <div class="config-section is-example">
  842. <div class="config-section-title">规则示例</div>
  843. <div class="config-example-list">
  844. <div class="config-example-row">
  845. <span>原始值</span>
  846. <code>138 0013-8000</code>
  847. </div>
  848. <div class="config-example-row">
  849. <span>清洗后</span>
  850. <code>13800138000</code>
  851. </div>
  852. </div>
  853. </div>
  854. </template>
  855. <template v-else-if="currentActiveSelectedRule.key === 'absoluteValue'">
  856. <div class="config-section">
  857. <div class="config-section-title">参数配置</div>
  858. <div class="config-check-list single">
  859. <Checkbox v-model:checked="absoluteValueChecked">将当前列转换为绝对值</Checkbox>
  860. </div>
  861. <div class="config-hint">仅对数值型内容生效,负数会转换为对应正值。</div>
  862. <div class="config-actions">
  863. <Button type="primary" size="small" @click="applyAbsoluteValueRule">应用规则</Button>
  864. </div>
  865. </div>
  866. <div class="config-section is-example">
  867. <div class="config-section-title">规则示例</div>
  868. <div class="config-example-list">
  869. <div class="config-example-row">
  870. <span>原始值</span>
  871. <code>-20</code>
  872. </div>
  873. <div class="config-example-row">
  874. <span>清洗后</span>
  875. <code>20</code>
  876. </div>
  877. </div>
  878. </div>
  879. </template>
  880. <template v-else-if="currentActiveSelectedRule.key === 'decimalConversion'">
  881. <div class="config-section">
  882. <div class="config-section-title">参数配置</div>
  883. <div class="config-check-list single">
  884. <Checkbox v-model:checked="decimalEnabled">启用进制转换</Checkbox>
  885. </div>
  886. <div class="config-form-grid">
  887. <div class="config-field">
  888. <span class="config-field-label">当前进制</span>
  889. <Select v-model:value="decimalSourceBase" :options="decimalBaseOptions" />
  890. </div>
  891. <div class="config-field">
  892. <span class="config-field-label">转换后进制</span>
  893. <Select v-model:value="decimalTargetBase" :options="decimalBaseOptions" />
  894. </div>
  895. </div>
  896. <div class="config-actions">
  897. <Button type="primary" size="small" @click="applyDecimalConversionRule">应用规则</Button>
  898. </div>
  899. </div>
  900. <div class="config-section is-example">
  901. <div class="config-section-title">规则示例</div>
  902. <div class="config-example-list">
  903. <div class="config-example-row">
  904. <span>原始值</span>
  905. <code>56(10进制)</code>
  906. </div>
  907. <div class="config-example-row">
  908. <span>清洗后</span>
  909. <code>38(16进制)</code>
  910. </div>
  911. </div>
  912. </div>
  913. </template>
  914. <template v-else-if="currentActiveSelectedRule.key === 'replaceValue'">
  915. <div class="config-section">
  916. <div class="config-section-title">参数配置</div>
  917. <div class="config-check-list single">
  918. <Checkbox v-model:checked="replaceEnabled">启用条件替换</Checkbox>
  919. </div>
  920. <div class="config-field">
  921. <span class="config-field-label">匹配方式</span>
  922. <Radio.Group v-model:value="replaceMode" class="config-radio-group">
  923. <Radio v-for="item in replaceModeOptions" :key="item.value" :value="item.value">
  924. {{ item.label }}
  925. </Radio>
  926. </Radio.Group>
  927. </div>
  928. <div class="config-form-grid">
  929. <div class="config-field">
  930. <span class="config-field-label">源值</span>
  931. <Input v-model:value="replaceSource" placeholder="例如:未知" />
  932. </div>
  933. <div class="config-field">
  934. <span class="config-field-label">目标值</span>
  935. <Input v-model:value="replaceTarget" placeholder="例如:--" />
  936. </div>
  937. </div>
  938. <div class="config-actions">
  939. <Button type="primary" size="small" @click="applyReplaceRule">应用规则</Button>
  940. </div>
  941. </div>
  942. <div class="config-section is-example">
  943. <div class="config-section-title">规则示例</div>
  944. <div class="config-example-list">
  945. <div class="config-example-row">
  946. <span>包含“未知”</span>
  947. <code>未知开户地址 -&gt; --开户地址</code>
  948. </div>
  949. <div class="config-example-row">
  950. <span>等于“空”</span>
  951. <code>空 -&gt; --</code>
  952. </div>
  953. </div>
  954. </div>
  955. </template>
  956. <template v-else-if="currentActiveSelectedRule.key === 'groupingColumns'">
  957. <div class="config-section">
  958. <div class="config-section-title">参数配置</div>
  959. <div class="config-hint">
  960. 仅建议用于新增列。按配置顺序拼接多个来源列的前/后 N 位内容,点击“应用规则”后生效。
  961. </div>
  962. <div v-if="!isCurrentColumnAdded" class="config-alert">
  963. 当前列不是新增列,组合列规则通常用于新增列结果字段。
  964. </div>
  965. <div class="grouping-list">
  966. <div class="grouping-row" v-for="(item, idx) in groupingColumnsDraft" :key="`group-${idx}`">
  967. <div class="config-field">
  968. <span class="config-field-label">来源表头</span>
  969. <Select
  970. v-model:value="item.header"
  971. :options="groupingHeaderOptions"
  972. placeholder="请选择表头"
  973. @change="(value) => handleGroupingColumnHeaderChange(idx, String(value || ''))"
  974. />
  975. </div>
  976. <div class="config-field">
  977. <span class="config-field-label">提取方向</span>
  978. <Radio.Group v-model:value="item.direction" class="config-radio-group compact">
  979. <Radio value="前">前</Radio>
  980. <Radio value="后">后</Radio>
  981. </Radio.Group>
  982. </div>
  983. <div class="config-field">
  984. <span class="config-field-label">位数</span>
  985. <InputNumber v-model:value="item.count" :min="1" :precision="0" />
  986. </div>
  987. <Button size="small" class="grouping-remove-btn" @click="removeGroupingColumnDraft(idx)">
  988. 删除
  989. </Button>
  990. </div>
  991. </div>
  992. <div class="config-actions between">
  993. <Button size="small" @click="addGroupingColumnDraft">新增一行</Button>
  994. <Button type="primary" size="small" @click="applyGroupingColumnsRule">应用规则</Button>
  995. </div>
  996. </div>
  997. <div class="config-section is-example">
  998. <div class="config-section-title">规则示例</div>
  999. <div class="config-example-list">
  1000. <div class="config-example-row">
  1001. <span>配置方式</span>
  1002. <code>姓名前 1 位 + 手机号后 4 位</code>
  1003. </div>
  1004. <div class="config-example-row">
  1005. <span>组合结果</span>
  1006. <code>张2369</code>
  1007. </div>
  1008. </div>
  1009. </div>
  1010. </template>
  1011. <template v-else-if="currentActiveSelectedRule.key === 'magnificationAdjustment'">
  1012. <div class="config-section">
  1013. <div class="config-section-title">参数配置</div>
  1014. <div class="config-field">
  1015. <span class="config-field-label">放大倍数</span>
  1016. <InputNumber v-model:value="magnificationValue" :min="1" />
  1017. <div class="config-hint">
  1018. 当前规则会按照“原值 × 倍数”输出,适合金额或比例类字段统一换算。
  1019. </div>
  1020. </div>
  1021. <div class="config-actions">
  1022. <Button type="primary" size="small" @click="applyMagnificationRule">应用规则</Button>
  1023. </div>
  1024. </div>
  1025. <div class="config-section is-example">
  1026. <div class="config-section-title">规则示例</div>
  1027. <div class="config-example-list">
  1028. <div class="config-example-row">
  1029. <span>原始值</span>
  1030. <code>1.23</code>
  1031. </div>
  1032. <div class="config-example-row">
  1033. <span>倍数 100</span>
  1034. <code>123</code>
  1035. </div>
  1036. </div>
  1037. </div>
  1038. </template>
  1039. <template v-else-if="currentActiveSelectedRule.key === 'yearComplete'">
  1040. <div class="config-section">
  1041. <div class="config-section-title">规则说明</div>
  1042. <div class="config-desc-list">
  1043. <div class="config-desc-item">当日期值缺少年份时,系统会按当前年份自动补全年份。</div>
  1044. <div class="config-desc-item">若补全年份后的日期晚于当前时间,则会自动回退到上一年。</div>
  1045. </div>
  1046. <div class="config-actions">
  1047. <Button type="primary" size="small" @click="applyYearCompleteRule">确认启用</Button>
  1048. </div>
  1049. </div>
  1050. <div class="config-section is-example">
  1051. <div class="config-section-title">规则示例</div>
  1052. <div class="config-example-list">
  1053. <div class="config-example-row">
  1054. <span>原始值</span>
  1055. <code>02-23 12:00:00</code>
  1056. </div>
  1057. <div class="config-example-row">
  1058. <span>补全年份后</span>
  1059. <code>2026-02-23 12:00:00</code>
  1060. </div>
  1061. </div>
  1062. </div>
  1063. </template>
  1064. </div>
  1065. </template>
  1066. <template v-else>
  1067. <div v-if="currentRuleExample" class="config-rule-shell is-example-preview">
  1068. <div class="config-rule-head">
  1069. <div class="config-rule-title-row">
  1070. <div class="config-rule-copy">
  1071. <div class="config-rule-title">{{ currentRuleExample.title }}</div>
  1072. <div class="config-rule-desc">{{ currentRuleExample.description }}</div>
  1073. </div>
  1074. <div class="config-rule-meta">
  1075. <span class="config-rule-state is-example">示例</span>
  1076. <span class="config-rule-tag" :class="`is-${currentRuleScopeType}`">
  1077. {{ currentRuleScopeLabel }}
  1078. </span>
  1079. </div>
  1080. </div>
  1081. </div>
  1082. <div class="config-section is-example">
  1083. <div class="config-section-title">规则示例</div>
  1084. <div class="config-example-list">
  1085. <div v-for="row in currentRuleExample.rows" :key="row.label" class="config-example-row">
  1086. <span>{{ row.label }}</span>
  1087. <code>{{ row.value }}</code>
  1088. </div>
  1089. </div>
  1090. </div>
  1091. <div class="config-preview-tip">
  1092. 双击左侧规则或点击“&gt;”添加到“已选规则”后,可配置参数并应用到当前列。
  1093. </div>
  1094. </div>
  1095. <div v-else class="config-empty-state">
  1096. <div class="empty-title">规则配置说明</div>
  1097. <div class="empty-text">
  1098. 当前规则:{{ currentRuleTitle || '未选择' }}({{ currentActiveRuleDisplayName }})
  1099. </div>
  1100. <div class="empty-tip">请先从左侧选择规则并添加到“已选规则”,再在此处进行参数配置。</div>
  1101. </div>
  1102. </template>
  1103. </div>
  1104. </div>
  1105. </div>
  1106. </div>
  1107. </div>
  1108. </section>
  1109. </div>
  1110. <Modal
  1111. v-model:open="addColumnDialogOpen"
  1112. :footer="null"
  1113. :width="704"
  1114. :centered="true"
  1115. @cancel="resetAddColumnForm"
  1116. wrapClassName="cleaning-config-modal add-column-modal"
  1117. >
  1118. <template #title>
  1119. <div class="cleaning-modal-title">
  1120. <span class="title-main">{{ addColumnMode === 'add' ? '新增列' : '编辑列' }}</span>
  1121. <span class="title-desc">支持引用原始列或录入固定文本,快速生成新的目标列。</span>
  1122. </div>
  1123. </template>
  1124. <div class="add-col-dialog addColumn">
  1125. <div class="addColumn-name">
  1126. <div class="row">
  1127. <span class="label required">{{ addColumnMode === 'add' ? '新增列名' : '编辑列名' }}:</span>
  1128. <Input v-model:value="addColumnForm.name" class="name-input" placeholder="请输入列名" />
  1129. </div>
  1130. </div>
  1131. <div class="addColumn-tip">双击左侧原始数据列即可加入右侧新列内容,右侧支持上下调整拼接顺序。</div>
  1132. <div class="addColumn-content">
  1133. <div class="addColumn-content-left">
  1134. <div class="content-card-head">
  1135. <div class="content-left-tip">原始数据列名</div>
  1136. <div class="content-card-desc">从当前数据表中选择可引用字段</div>
  1137. </div>
  1138. <div class="left-oldColumn">
  1139. <div
  1140. v-for="field in sourceFieldCandidates"
  1141. :key="field"
  1142. class="oldColumn-box"
  1143. :title="field"
  1144. :class="{ 'active-item': selectedSourceField === field }"
  1145. @click="selectedSourceField = field"
  1146. @dblclick="appendSourceField(field)"
  1147. >
  1148. {{ field }}
  1149. </div>
  1150. </div>
  1151. </div>
  1152. <div class="addColumn-content-right">
  1153. <div class="right-header">
  1154. <div class="content-card-head">
  1155. <div class="content-right-tip">新列内容</div>
  1156. <div class="content-card-desc">
  1157. {{ addColumnForm.mode === 'ref' ? '支持拖动排序并删除单个字段' : '可直接输入固定文本作为新列内容' }}
  1158. </div>
  1159. </div>
  1160. </div>
  1161. <div class="right-radio">
  1162. <Radio.Group v-model:value="addColumnForm.mode">
  1163. <Radio value="ref">引用已有列</Radio>
  1164. <Radio value="custom">自定义列内容</Radio>
  1165. </Radio.Group>
  1166. </div>
  1167. <div class="right-newColumn" v-show="addColumnForm.mode === 'ref'">
  1168. <div
  1169. v-for="(field, index) in addColumnForm.contentFields"
  1170. :key="`${field}-${index}`"
  1171. class="newColumn-box"
  1172. :class="{
  1173. 'active-item': selectedContentIndex === index,
  1174. 'is-dragging': draggingContentIndex === index,
  1175. 'is-drag-over': dragOverContentIndex === index && draggingContentIndex !== index,
  1176. }"
  1177. draggable="true"
  1178. @click="selectedContentIndex = index"
  1179. @dragstart="handleContentDragStart(index, $event)"
  1180. @dragenter="handleContentDragEnter(index)"
  1181. @dragover="handleContentDragOver(index, $event)"
  1182. @drop="handleContentDrop(index, $event)"
  1183. @dragend="handleContentDragEnd"
  1184. >
  1185. <span class="newColumn-text" :title="field">{{ field }}</span>
  1186. <span class="del-icon" @click.stop="removeContentField(index)">×</span>
  1187. </div>
  1188. </div>
  1189. <div class="right-input" v-show="addColumnForm.mode === 'custom'">
  1190. <Input
  1191. v-model:value="addColumnForm.customContent"
  1192. :maxlength="100"
  1193. placeholder="请输入自定义列内容"
  1194. style="height: 32px; line-height: 32px"
  1195. />
  1196. </div>
  1197. </div>
  1198. </div>
  1199. <div class="dialog-footer">
  1200. <Button class="add-col-cancel" @click="resetAddColumnForm">取消</Button>
  1201. <Button type="primary" class="add-col-confirm" @click="confirmAddOrEditColumn">确定</Button>
  1202. </div>
  1203. </div>
  1204. </Modal>
  1205. <Modal
  1206. v-model:open="splitColumnDialogOpen"
  1207. :width="460"
  1208. :centered="true"
  1209. okText="确定"
  1210. cancelText="取消"
  1211. @ok="confirmSplitColumn"
  1212. @cancel="resetSplitForm"
  1213. wrapClassName="cleaning-config-modal split-column-modal"
  1214. >
  1215. <template #title>
  1216. <div class="cleaning-modal-title">
  1217. <span class="title-main">拆分列</span>
  1218. <span class="title-desc">按字符长度或特殊标识将单列拆分为两个目标列,适合姓名、地址等复合字段。</span>
  1219. </div>
  1220. </template>
  1221. <div class="split-column-form">
  1222. <div class="split-column-tip">选择需要拆分的原始列,并填写拆分后的列名与拆分截止标识。</div>
  1223. <div class="split-form-card">
  1224. <div class="form-row">
  1225. <span class="form-label">选择需要拆分的列:</span>
  1226. <Select
  1227. v-model:value="splitColumnForm.source"
  1228. :options="splitSourceOptions"
  1229. placeholder="请选择"
  1230. class="form-input"
  1231. />
  1232. </div>
  1233. <div class="form-row">
  1234. <span class="form-label required">拆分后列名1:</span>
  1235. <Input v-model:value="splitColumnForm.targetName1" placeholder="请输入" class="form-input" />
  1236. </div>
  1237. <div class="form-row">
  1238. <span class="form-label required">拆分后列名2:</span>
  1239. <Input v-model:value="splitColumnForm.targetName2" placeholder="请输入" class="form-input" />
  1240. </div>
  1241. <div class="form-row">
  1242. <span class="form-label required">拆分截止标识:</span>
  1243. <div class="delimiter-group">
  1244. <Select
  1245. v-model:value="splitColumnForm.delimiterType"
  1246. :options="delimiterOptions"
  1247. class="delimiter-select"
  1248. />
  1249. <Input
  1250. v-model:value="splitColumnForm.delimiterValue"
  1251. :placeholder="splitColumnForm.delimiterType === 'length' ? '请输入字符长度' : '请输入特殊标识'"
  1252. class="delimiter-input"
  1253. />
  1254. </div>
  1255. </div>
  1256. </div>
  1257. </div>
  1258. </Modal>
  1259. <Modal v-model:open="deleteColumnDialogOpen" title="删除列" :width="520" :centered="true" @ok="confirmDeleteColumn">
  1260. <div>确认删除列「{{ selectedAddedColumn || '-' }}」?</div>
  1261. </Modal>
  1262. <Modal
  1263. v-model:open="templateModalOpen"
  1264. :footer="null"
  1265. width="1080px"
  1266. :centered="true"
  1267. wrapClassName="select-template-modal"
  1268. :destroyOnClose="true"
  1269. >
  1270. <template #title>
  1271. <div class="cleaning-modal-title">
  1272. <span class="title-main">选择模板</span>
  1273. </div>
  1274. </template>
  1275. <div class="template-modal-layout">
  1276. <aside class="modal-left">
  1277. <div class="modal-panel-head">
  1278. <div>
  1279. <div class="panel-title">模板分类</div>
  1280. </div>
  1281. <span class="head-badge">{{ Math.max(categories.length - 1, 0) }} 类</span>
  1282. </div>
  1283. <Button type="primary" block class="create-template-btn">+创建新模板</Button>
  1284. <div class="cate-list">
  1285. <div
  1286. v-for="cate in categories"
  1287. :key="cate"
  1288. class="cate-item"
  1289. :class="{ active: activeCategory === cate }"
  1290. @click="activeCategory = cate"
  1291. >
  1292. <span class="cate-name">{{ cate }}</span>
  1293. <span class="cate-count">{{ getCategoryCount(cate) }}</span>
  1294. </div>
  1295. </div>
  1296. </aside>
  1297. <section class="modal-mid">
  1298. <div class="modal-panel-head">
  1299. <div>
  1300. <div class="panel-title">模板列表</div>
  1301. </div>
  1302. <div class="modal-panel-head__actions">
  1303. <div class="tab-row" v-show="showTemplateTabSwitch">
  1304. <span
  1305. class="tab"
  1306. :class="{ active: activeTemplateTab === 'template' }"
  1307. @click="activeTemplateTab = 'template'"
  1308. >模板</span
  1309. >
  1310. <span
  1311. class="tab"
  1312. :class="{ active: activeTemplateTab === 'plugin' }"
  1313. @click="activeTemplateTab = 'plugin'"
  1314. >模板插件</span
  1315. >
  1316. </div>
  1317. <span class="head-badge">{{ filteredTemplateList.length }} 个</span>
  1318. </div>
  1319. </div>
  1320. <div class="filters">
  1321. <div class="filter-item filter-item--grow">
  1322. <span class="filter-label">模板名称</span>
  1323. <Input v-model:value="templateKeyword" placeholder="输入模板名称查询" />
  1324. </div>
  1325. <div class="filter-item">
  1326. <span class="filter-label">来源</span>
  1327. <Select v-model:value="templateSource" :options="templateSourceOptions" class="source-select" />
  1328. </div>
  1329. </div>
  1330. <div class="list-summary">
  1331. <span>当前范围:{{ activeTemplateTab === 'plugin' ? '模板插件' : activeCategory }}</span>
  1332. <span>已选:{{ modalTemplateItem ? getTemplateDisplayName(modalTemplateItem) : '未选择' }}</span>
  1333. </div>
  1334. <div class="tpl-list">
  1335. <div
  1336. v-for="tpl in filteredTemplateList"
  1337. :key="String(tpl.id)"
  1338. class="tpl-item"
  1339. :class="{ active: modalTemplateItem?.id === tpl.id }"
  1340. @click="handleTemplateItemClick(tpl)"
  1341. @dblclick="handleTemplateItemConfirm(tpl)"
  1342. >
  1343. <div class="tpl-main">
  1344. <div class="name">{{ getTemplateDisplayName(tpl) }}</div>
  1345. <div class="tpl-meta">
  1346. <span class="tpl-meta-text">{{ tpl.classify || '未分类' }}</span>
  1347. <span class="tpl-meta-text">{{ getTemplateSourceLabel(tpl) }}</span>
  1348. <span class="tpl-meta-text">表头 {{ getTemplateHeaderCount(tpl) }} 项</span>
  1349. </div>
  1350. </div>
  1351. <span v-if="modalTemplateItem?.id === tpl.id" class="selected-mark">已选</span>
  1352. </div>
  1353. <div v-if="!filteredTemplateList.length" class="empty">暂无模板</div>
  1354. </div>
  1355. </section>
  1356. <section class="modal-right">
  1357. <div class="modal-panel-head">
  1358. <div>
  1359. <div class="panel-title">模板规则预览</div>
  1360. </div>
  1361. <span class="head-badge">{{ previewRules.length }} 条</span>
  1362. </div>
  1363. <template v-if="modalPreviewTemplate">
  1364. <div class="preview-overview">
  1365. <div class="overview-card">
  1366. <span class="overview-label">模板名称</span>
  1367. <span class="overview-value">{{ getTemplateDisplayName(modalPreviewTemplate) }}</span>
  1368. </div>
  1369. <div class="overview-card">
  1370. <span class="overview-label">模板来源</span>
  1371. <span class="overview-value">{{ modalTemplateSourceLabel }}</span>
  1372. </div>
  1373. <div class="overview-card">
  1374. <span class="overview-label">分类</span>
  1375. <span class="overview-value">{{ modalPreviewTemplate.classify || '未分类' }}</span>
  1376. </div>
  1377. <div class="overview-card">
  1378. <span class="overview-label">字段数</span>
  1379. <span class="overview-value">{{ modalTemplateFieldCount }}</span>
  1380. </div>
  1381. </div>
  1382. <div class="preview-meta">
  1383. <div class="preview-meta-row preview-meta-row--headers">
  1384. <span class="meta-label">表头预览</span>
  1385. <span class="meta-value meta-value--scroll">
  1386. {{ modalTemplateHeaderList.length ? modalTemplateHeaderList.join(' / ') : '暂无表头信息' }}
  1387. </span>
  1388. </div>
  1389. </div>
  1390. <div class="preview-rule-title">模板规则</div>
  1391. <div class="preview-rule-table">
  1392. <div class="rule-head">
  1393. <span>目标字段</span>
  1394. <span>原始字段</span>
  1395. </div>
  1396. <div class="rule-row" v-for="(row, idx) in previewRules" :key="idx">
  1397. <span class="rule-cell rule-cell--target">
  1398. <span class="rule-target-label" :title="row.targetField">
  1399. <span v-if="row.required" class="rule-required-mark">*</span>
  1400. <span class="rule-target-text">{{ row.targetField }}</span>
  1401. </span>
  1402. <span v-if="row.required" class="rule-required-badge">必填</span>
  1403. </span>
  1404. <span class="rule-cell" :title="row.originalField">{{ row.originalField }}</span>
  1405. </div>
  1406. <div v-if="templatePreviewLoading" class="empty">模板详情加载中...</div>
  1407. <div v-else-if="!previewRules.length" class="empty">暂无规则</div>
  1408. </div>
  1409. </template>
  1410. <template v-else>
  1411. <div class="preview-empty">
  1412. <div class="empty-title">请选择模板</div>
  1413. <div class="empty-desc">左侧选择分类,中间点击具体模板后,这里会展示字段规则与基础信息。</div>
  1414. </div>
  1415. </template>
  1416. <div class="modal-actions">
  1417. <Button type="primary" :disabled="!modalTemplateItem || isPluginTemplateTab" @click="confirmTemplateSelect">
  1418. 确认选择
  1419. </Button>
  1420. <Button @click="closeTemplateModal">取消</Button>
  1421. </div>
  1422. </section>
  1423. </div>
  1424. </Modal>
  1425. </div>
  1426. </template>
  1427. <script lang="ts" setup name="ViewsDataCleaning">
  1428. import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
  1429. import { DownOutlined, UpOutlined } from '@ant-design/icons-vue';
  1430. import { Button, Checkbox, Input, InputNumber, Modal, Popover, Radio, Select, Switch, Tooltip } from 'ant-design-vue';
  1431. import { EmptyState } from '@jeesite/core/components/EmptyState';
  1432. import { BasicTree } from '@jeesite/core/components/Tree';
  1433. import { useDesign } from '@jeesite/core/hooks/web/useDesign';
  1434. import { useGo, useQuery } from '@jeesite/core/hooks/web/usePage';
  1435. import { PageEnum } from '@jeesite/core/enums/pageEnum';
  1436. import { useMessage } from '@jeesite/core/hooks/web/useMessage';
  1437. import type { FileInfo } from '@jeesite/types/api';
  1438. import { tplCategoryList, tplCheckTableName, tplDetail, tplPage } from '@jeesite/plat/api/tpl/tplApi';
  1439. import type { TableInfo } from '@jeesite/plat/api/tpl/types/dto';
  1440. import {
  1441. FunAbs,
  1442. FunDate,
  1443. FunDateTime,
  1444. FunExtra,
  1445. FunMultiplier,
  1446. FunRadix,
  1447. FunRegular,
  1448. FunReplace,
  1449. FunSpExtra,
  1450. FunTime,
  1451. MappingEnum,
  1452. RuleMapping,
  1453. RuleSplit,
  1454. } from '../../../dm/types/Func';
  1455. import {
  1456. buildRemoveSpecialRegex,
  1457. groupColumnsFnValue,
  1458. normalizeDecimalBaseLabel,
  1459. runCleaningPreviewPipeline,
  1460. type CleaningRuleKey as RulePipelineKey,
  1461. } from './cleaningRuleUtils';
  1462. import { resetImportProgressSession } from './importProgressSession';
  1463. import { FileStatusEnum } from '../../types/enum';
  1464. type TreeTableRuleInfo = {
  1465. id?: string | number | null;
  1466. tableId?: string | number | null;
  1467. fieldNameCn?: string | null;
  1468. fieldNameEn?: string | null;
  1469. fieldType?: string | null;
  1470. required?: number | null;
  1471. fieldLen?: number | null;
  1472. matched?: number | null;
  1473. matchedRequired?: number | null;
  1474. directionConf?: string | null;
  1475. zf?: number | string | boolean | null;
  1476. fieldSort?: number | null;
  1477. fileColIndex?: number | null;
  1478. fileColName?: string | null;
  1479. funList?: TreeStoredFunRule[] | null;
  1480. columnRule?: TreeStoredColumnRule | null;
  1481. };
  1482. type TreeStoredFunRule = {
  1483. '@class'?: string;
  1484. key?: string;
  1485. ruleKey?: string;
  1486. name?: string;
  1487. sort?: number | null;
  1488. rules?: Record<string, any>;
  1489. [key: string]: any;
  1490. };
  1491. type TreeStoredColumnRule = {
  1492. '@class'?: string;
  1493. mode?: 'ref' | 'custom' | 'split';
  1494. contentFields?: string[];
  1495. customContent?: string;
  1496. source?: string;
  1497. delimiterType?: string;
  1498. delimiterValue?: string;
  1499. targetName1?: string;
  1500. targetName2?: string;
  1501. refIndex?: number | number[] | null;
  1502. mappingEnum?: number | null;
  1503. splitNameIndex?: number | null;
  1504. splitNameIndex2?: number | null;
  1505. content?: string | null;
  1506. len?: number | null;
  1507. splitChar?: string | null;
  1508. [key: string]: any;
  1509. };
  1510. type TreeFileInfo = Omit<
  1511. FileInfo,
  1512. | 'id'
  1513. | 'pid'
  1514. | 'children'
  1515. | 'tableName'
  1516. | 'templateId'
  1517. | 'fileType'
  1518. | 'dataNum'
  1519. | 'fileSize'
  1520. | 'fileStatus'
  1521. | 'lineNo'
  1522. | 'batchId'
  1523. | 'successNum'
  1524. | 'failNum'
  1525. | 'charSet'
  1526. | 'delimiter'
  1527. | 'header'
  1528. | 'tableRuleList'
  1529. > & {
  1530. id?: string | number | null;
  1531. pid?: string | number | null;
  1532. tableName?: string | null;
  1533. templateId?: string | number | null;
  1534. fileType?: string | null;
  1535. dataNum?: string | number | null;
  1536. fileSize?: string | number | null;
  1537. fileStatus?: string | null;
  1538. lineNo?: number | null;
  1539. batchId?: string | number | null;
  1540. successNum?: string | number | null;
  1541. failNum?: string | number | null;
  1542. charSet?: string | null;
  1543. delimiter?: string | null;
  1544. header?: string | string[] | null;
  1545. tableRuleList?: TreeTableRuleInfo[] | null;
  1546. ignoreLines?: number[] | null;
  1547. groupColumns?: Array<{ targetHeader: string; groupFlag: boolean; groupColumns: Array<Record<string, any>> }> | null;
  1548. funcRegx?: string | null;
  1549. childTemplateName?: string | null;
  1550. children?: TreeFileInfo[];
  1551. };
  1552. type PreviewFileDataset = {
  1553. headers: string[];
  1554. rows: Record<string, string>[];
  1555. };
  1556. type TemplateListItem = TableInfo & { source?: number; hasInner?: string | number | boolean | null };
  1557. type MappingTargetField = { key: string; label: string; required: boolean };
  1558. type FormatValidationType =
  1559. | 'number'
  1560. | 'date'
  1561. | 'dateTime'
  1562. | 'time'
  1563. | 'loanFlag'
  1564. | 'callType'
  1565. | 'phone'
  1566. | 'acctCardNo'
  1567. | 'idCardNumber';
  1568. type RecognitionDirectionComparableMap = Record<RecognitionRuleTabKey, Set<string>>;
  1569. type HeaderFormatValidationState = {
  1570. invalidTargets: Array<{
  1571. key: string;
  1572. label: string;
  1573. formatType: FormatValidationType;
  1574. invalidValue: string;
  1575. }>;
  1576. title: string;
  1577. };
  1578. type RecognitionBasicInfoRow = {
  1579. key: string;
  1580. headerMatch: string;
  1581. targetField: string;
  1582. endFlag: string;
  1583. };
  1584. type RecognitionDirectionItem = {
  1585. key: string;
  1586. label: string;
  1587. value: string;
  1588. };
  1589. type RecognitionRuleTabKey = '借贷标志' | '通信类别';
  1590. type RecognitionDirectionField = {
  1591. key: string;
  1592. label: string;
  1593. tab: RecognitionRuleTabKey;
  1594. entries: RecognitionDirectionItem[];
  1595. };
  1596. const { prefixCls } = useDesign('case-data-cleaning');
  1597. const go = useGo();
  1598. const pageQuery = useQuery();
  1599. const { createMessage } = useMessage();
  1600. const importingData = ref(false);
  1601. const leftCollapsed = ref(false);
  1602. const leftPanelWidth = ref(300);
  1603. const middlePanelWidth = ref(520);
  1604. const minLeftWidth = 220;
  1605. const maxLeftWidth = 520;
  1606. const minMiddleWidth = 360;
  1607. const maxMiddleWidth = 900;
  1608. const minRightWidth = 560;
  1609. const resizeState = ref<{ type: 'left' | 'middle' | ''; startX: number; startLeft: number; startMiddle: number }>({
  1610. type: '',
  1611. startX: 0,
  1612. startLeft: 300,
  1613. startMiddle: 520,
  1614. });
  1615. const pendingClientX = ref<number | null>(null);
  1616. const rafId = ref<number | null>(null);
  1617. const layoutTemplateColumns = computed(() => {
  1618. if (leftCollapsed.value) {
  1619. return `${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
  1620. }
  1621. return `${leftPanelWidth.value}px 6px ${middlePanelWidth.value}px 6px minmax(${minRightWidth}px, 1fr)`;
  1622. });
  1623. const sortMode = ref<'asc' | 'desc' | ''>('');
  1624. const selectedKeys = ref<string[]>([]);
  1625. interface FileTreeNode {
  1626. key: string;
  1627. title: string;
  1628. children?: FileTreeNode[];
  1629. raw?: TreeFileInfo;
  1630. selectable?: boolean;
  1631. }
  1632. type SheetNodeVisualState = 'success' | 'fail' | 'analyzing';
  1633. const fileStatusLabelMap = Object.entries(FileStatusEnum).reduce(
  1634. (acc, [key, label]) => {
  1635. acc[key] = label;
  1636. return acc;
  1637. },
  1638. {} as Record<string, string>,
  1639. );
  1640. const normalizeNodeId = (value?: string | number | null, fallback = '') => {
  1641. const normalized = String(value ?? '').trim();
  1642. return normalized || fallback;
  1643. };
  1644. const normalizeParentId = (value?: string | number | null) => {
  1645. const normalized = String(value ?? '').trim();
  1646. return !normalized || normalized === '0' ? '' : normalized;
  1647. };
  1648. const isFileRootItem = (item: TreeFileInfo) => !normalizeParentId(item.pid);
  1649. const hasChildItems = (item: TreeFileInfo) => Array.isArray(item.children) && item.children.length > 0;
  1650. const getFallbackTreeKey = (item: TreeFileInfo) =>
  1651. [
  1652. item.filePath || item.fileName || 'file',
  1653. item.sheetName || 'root',
  1654. item.tableName || '',
  1655. item.lineNo ?? '',
  1656. item.createDateTime || '',
  1657. ].join('__');
  1658. const getTreeNodeKey = (item: TreeFileInfo) => normalizeNodeId(item.id, getFallbackTreeKey(item));
  1659. const normalizeTemplateId = (value?: string | number | null) => {
  1660. const next = Number(value);
  1661. return Number.isFinite(next) && next > 0 ? next : 0;
  1662. };
  1663. const getRequestErrorMessage = (error: any, fallback = '操作失败') =>
  1664. error?.response?.data?.message || error?.message || fallback;
  1665. const normalizeTemplateHeaders = (headers?: string | string[] | null) => {
  1666. if (Array.isArray(headers)) {
  1667. return headers.map((item) => String(item ?? '').trim()).filter(Boolean);
  1668. }
  1669. return String(headers || '')
  1670. .split(/[,,]/)
  1671. .map((item) => item.trim())
  1672. .filter(Boolean);
  1673. };
  1674. const createTemplateFieldKey = (value: string | undefined, index: number) => {
  1675. const normalized = String(value || '').trim();
  1676. return normalized ? `field_${index + 1}_${normalized}` : `field_${index + 1}`;
  1677. };
  1678. const normalizeRuleText = (value?: unknown) => String(value ?? '').trim();
  1679. const normalizeTemplateClassifyText = (value?: unknown) => normalizeRuleText(value);
  1680. const normalizeTemplateNameKey = (value?: unknown) => normalizeRuleText(value).toLocaleLowerCase();
  1681. const normalizeRecognitionConfigText = (value?: unknown) => String(value ?? '').trim();
  1682. const normalizeSingleLineText = (value?: unknown) =>
  1683. String(value ?? '')
  1684. .replace(/[\t\r\n\s\u3000]+/g, '')
  1685. .trim();
  1686. const resolveTemplateNameCheckResult = (payload: unknown): boolean | null => {
  1687. if (typeof payload === 'boolean') return payload;
  1688. if (typeof payload === 'number') {
  1689. if (payload === 1) return true;
  1690. if (payload === 0) return false;
  1691. return null;
  1692. }
  1693. if (typeof payload === 'string') {
  1694. const normalized = payload.trim().toLowerCase();
  1695. if (['true', '1', 'yes', 'y', 'ok', 'success'].includes(normalized)) return true;
  1696. if (['false', '0', 'no', 'n', 'fail'].includes(normalized)) return false;
  1697. return null;
  1698. }
  1699. if (!payload || typeof payload !== 'object') return null;
  1700. const record = payload as Record<string, unknown>;
  1701. for (const key of ['valid', 'available', 'pass', 'result', 'data']) {
  1702. const resolved = resolveTemplateNameCheckResult(record[key]);
  1703. if (resolved !== null) return resolved;
  1704. }
  1705. const existsResult = resolveTemplateNameCheckResult(record.exists);
  1706. if (existsResult !== null) return !existsResult;
  1707. return null;
  1708. };
  1709. const createRecognitionBasicInfoKey = () => `merge_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
  1710. const normalizeFieldMatchText = (value?: string | number | null) =>
  1711. normalizeRuleText(value)
  1712. .replace(/[\s_\-—()()[\]【】{}<>《》"'`‘’“”、,,.。::;;/\\|]/g, '')
  1713. .toLowerCase();
  1714. const normalizeTableRuleList = (list?: TreeTableRuleInfo[] | null) =>
  1715. Array.isArray(list)
  1716. ? list.filter(
  1717. (item) =>
  1718. item && [item.fieldNameCn, item.fieldNameEn, item.fileColName].some((val) => normalizeRuleText(val)),
  1719. )
  1720. : [];
  1721. const getIntegerRuleFlag = (value: unknown) => {
  1722. const normalizedValue = Number(value);
  1723. return Number.isInteger(normalizedValue) ? normalizedValue : null;
  1724. };
  1725. const getRawRuleFlagValue = (
  1726. source: unknown,
  1727. key: 'required' | 'matched' | 'matchedRequired',
  1728. ): number | string | null | undefined => {
  1729. const record = (source || {}) as Record<string, any>;
  1730. if (!Object.prototype.hasOwnProperty.call(record, key)) return undefined;
  1731. const value = record[key];
  1732. if (value === undefined) return undefined;
  1733. if (value === null) return null;
  1734. const normalizedValue = Number(value);
  1735. return Number.isInteger(normalizedValue) ? normalizedValue : value;
  1736. };
  1737. const pickFirstDefinedRuleFlagValue = (...values: Array<number | string | null | undefined>) => {
  1738. for (const value of values) {
  1739. if (value !== undefined) {
  1740. return value;
  1741. }
  1742. }
  1743. return undefined;
  1744. };
  1745. const getTableRuleRequiredFlag = (item?: TreeTableRuleInfo | null, fallback = false) => {
  1746. if (!item) return fallback;
  1747. const required = getIntegerRuleFlag(item.required);
  1748. if (required !== null) {
  1749. return required === 1;
  1750. }
  1751. return fallback;
  1752. };
  1753. const getTableRuleMatchedDisplayFlag = (item?: TreeTableRuleInfo | null, fallback = false) => {
  1754. if (!item) return fallback;
  1755. const matched = getIntegerRuleFlag(item.matched);
  1756. if (matched !== null) {
  1757. return matched === 1;
  1758. }
  1759. const required = getIntegerRuleFlag(item.required);
  1760. if (required !== null) {
  1761. return required === 1;
  1762. }
  1763. return fallback;
  1764. };
  1765. const getTableRuleTargetLabel = (item: TreeTableRuleInfo, index: number) =>
  1766. normalizeRuleText(item.fieldNameCn) || normalizeRuleText(item.fieldNameEn) || `字段${index + 1}`;
  1767. const getTableRuleTargetKey = (item: TreeTableRuleInfo, index: number) =>
  1768. normalizeRuleText(item.fieldNameEn) || createTemplateFieldKey(getTableRuleTargetLabel(item, index), index);
  1769. const buildTargetFieldsFromRuleList = (list: TreeTableRuleInfo[]): MappingTargetField[] => {
  1770. const usedKeys = new Set<string>();
  1771. return normalizeTableRuleList(list).reduce((acc, item, index) => {
  1772. const key = getTableRuleTargetKey(item, index);
  1773. if (!key || usedKeys.has(key)) return acc;
  1774. usedKeys.add(key);
  1775. acc.push({
  1776. key,
  1777. label: getTableRuleTargetLabel(item, index),
  1778. required: getTableRuleRequiredFlag(item),
  1779. });
  1780. return acc;
  1781. }, [] as MappingTargetField[]);
  1782. };
  1783. const getTemplateFieldRequired = (value: unknown) => {
  1784. if (value === null || value === undefined || value === '') return false;
  1785. if (typeof value === 'boolean') return value;
  1786. const parsed = Number(value);
  1787. return !Number.isNaN(parsed) && parsed === 1;
  1788. };
  1789. const getTemplateFieldRecord = (item: unknown) => (item || {}) as Record<string, any>;
  1790. const getTemplateFieldRequiredFlag = (item: unknown, fallback = false) => {
  1791. const field = getTemplateFieldRecord(item);
  1792. if (Object.prototype.hasOwnProperty.call(field, 'required')) {
  1793. return getTemplateFieldRequired(field.required);
  1794. }
  1795. return fallback;
  1796. };
  1797. const getTemplateFieldMatchedDisplayFlag = (item: unknown, fallback = false) => {
  1798. const field = getTemplateFieldRecord(item);
  1799. if (Object.prototype.hasOwnProperty.call(field, 'matched')) {
  1800. return getTemplateFieldRequired(field.matched);
  1801. }
  1802. if (Object.prototype.hasOwnProperty.call(field, 'required')) {
  1803. return getTemplateFieldRequired(field.required);
  1804. }
  1805. return fallback;
  1806. };
  1807. const getOrderedTemplateFields = (fields?: TableInfo['tableFields'] | null) =>
  1808. (Array.isArray(fields) ? fields : [])
  1809. .filter((item) => {
  1810. const field = getTemplateFieldRecord(item);
  1811. return !!(field.fieldComment || field.fieldNameCn || field.fieldNameEn || field.fieldName);
  1812. })
  1813. .map((item, index) => {
  1814. const field = getTemplateFieldRecord(item);
  1815. const sort = Number(field.fieldSort);
  1816. return {
  1817. item,
  1818. index,
  1819. required: getTemplateFieldRequiredFlag(item),
  1820. sort: Number.isFinite(sort) && sort > 0 ? sort : index + 1,
  1821. };
  1822. })
  1823. .sort((a, b) => {
  1824. if (a.required !== b.required) {
  1825. return a.required ? -1 : 1;
  1826. }
  1827. return a.sort === b.sort ? a.index - b.index : a.sort - b.sort;
  1828. })
  1829. .map(({ item }) => item);
  1830. const getTemplateFieldTargetLabel = (item: unknown, index: number) => {
  1831. const field = getTemplateFieldRecord(item);
  1832. return String(field.fieldComment || field.fieldNameCn || field.fieldName || `字段${index + 1}`);
  1833. };
  1834. const getTemplateFieldTargetKey = (item: unknown, index: number) => {
  1835. const field = getTemplateFieldRecord(item);
  1836. return String(
  1837. field.fieldNameEn ||
  1838. field.fieldName ||
  1839. createTemplateFieldKey(String(field.fieldComment || field.fieldNameCn || ''), index),
  1840. );
  1841. };
  1842. const isTransAmountTargetField = (field: Pick<MappingTargetField, 'key' | 'label'>) =>
  1843. normalizeRuleText(field.key) === 'transAmount' || normalizeFieldMatchText(field.label).includes('交易金额');
  1844. const isLoanFlagTargetField = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
  1845. const targetKey = normalizeRuleText(field.key);
  1846. const targetLabel = normalizeRuleText(field.label);
  1847. return targetKey === 'debitCreditFlag' || targetKey === 'loanFlag' || targetLabel.includes('借贷');
  1848. };
  1849. const getZfFlag = (value: unknown) => {
  1850. if (typeof value === 'boolean') return value;
  1851. const normalizedValue = normalizeRuleText(value as string | number | null).toLowerCase();
  1852. return ['1', 'true', 'yes', '是'].includes(normalizedValue);
  1853. };
  1854. const getRecordZfValue = (source: unknown) => {
  1855. const record = (source || {}) as Record<string, any>;
  1856. if (!Object.prototype.hasOwnProperty.call(record, 'zf')) return undefined;
  1857. return record.zf === null || record.zf === undefined || record.zf === '' ? undefined : record.zf;
  1858. };
  1859. const getTableRuleTargetField = (item: TreeTableRuleInfo, index: number) => ({
  1860. key: getTableRuleTargetKey(item, index),
  1861. label: getTableRuleTargetLabel(item, index),
  1862. });
  1863. const findTransAmountTableRule = (list?: TreeTableRuleInfo[] | null) =>
  1864. normalizeTableRuleList(list).find((item, index) =>
  1865. isTransAmountTargetField(getTableRuleTargetField(item, index)),
  1866. ) || null;
  1867. const findTransAmountTemplateFieldMeta = (template?: Partial<TableInfo> | null) =>
  1868. getOrderedTemplateFields(template?.tableFields).find((item, index) =>
  1869. isTransAmountTargetField({
  1870. key: getTemplateFieldTargetKey(item, index),
  1871. label: getTemplateFieldTargetLabel(item, index),
  1872. }),
  1873. ) || null;
  1874. const buildTargetFieldsFromTemplateInfo = (template?: Partial<TableInfo> | null): MappingTargetField[] => {
  1875. const tableFields = getOrderedTemplateFields(template?.tableFields);
  1876. if (tableFields.length) {
  1877. return tableFields.map((item, index) => ({
  1878. key: getTemplateFieldTargetKey(item, index),
  1879. label: getTemplateFieldTargetLabel(item, index),
  1880. required: getTemplateFieldRequiredFlag(item),
  1881. }));
  1882. }
  1883. return [];
  1884. };
  1885. const inferTemplateClassify = (item: Pick<TreeFileInfo, 'fileName' | 'tableName' | 'sheetName'>) => {
  1886. const text = `${item.fileName || ''}${item.tableName || ''}${item.sheetName || ''}`;
  1887. if (text.includes('通信')) return '通信类';
  1888. if (text.includes('银行') || text.includes('账户') || text.includes('交易')) return '金融类';
  1889. if (text.includes('民航') || text.includes('铁路') || text.includes('旅馆') || text.includes('快递'))
  1890. return '轨迹类';
  1891. if (text.includes('公安') || text.includes('户籍') || text.includes('违法') || text.includes('护照'))
  1892. return '公安类';
  1893. return '未分类';
  1894. };
  1895. const FILE_INFO_STORAGE_KEY = 'case_clean_file_infos';
  1896. const IMPORT_FILE_INFO_STORAGE_KEY = 'case_clean_import_file_infos';
  1897. const mockFileList: TreeFileInfo[] = [];
  1898. const buildTreeFileInfoIdMap = (list: TreeFileInfo[]) => {
  1899. const infoMap = new Map<string, TreeFileInfo>();
  1900. const walk = (nodes?: TreeFileInfo[] | null) => {
  1901. if (!Array.isArray(nodes)) return;
  1902. nodes.forEach((node) => {
  1903. const nodeId = normalizeRuleText(node?.id);
  1904. if (nodeId) {
  1905. infoMap.set(nodeId, node);
  1906. }
  1907. if (Array.isArray(node?.children) && node.children.length) {
  1908. walk(node.children);
  1909. }
  1910. });
  1911. };
  1912. walk(list);
  1913. return infoMap;
  1914. };
  1915. const mockFileInfoById = buildTreeFileInfoIdMap(mockFileList);
  1916. const mergeStoredTableRuleListWithFallback = (
  1917. storedList?: TreeTableRuleInfo[] | null,
  1918. fallbackList?: TreeTableRuleInfo[] | null,
  1919. ) => {
  1920. const storedRules = normalizeTableRuleList(storedList);
  1921. const fallbackRules = normalizeTableRuleList(fallbackList);
  1922. if (!storedRules.length) {
  1923. return fallbackRules.map((item) => sanitizeTreeTableRuleInfo(item));
  1924. }
  1925. if (!fallbackRules.length) {
  1926. return storedRules.map((item) => sanitizeTreeTableRuleInfo(item));
  1927. }
  1928. const fallbackRuleMap = fallbackRules.reduce(
  1929. (acc, item, index) => {
  1930. const key = getTableRuleTargetKey(item, index);
  1931. const label = normalizeFieldMatchText(getTableRuleTargetLabel(item, index));
  1932. if (key) acc[key] = item;
  1933. if (label) acc[label] = item;
  1934. return acc;
  1935. },
  1936. {} as Record<string, TreeTableRuleInfo>,
  1937. );
  1938. return storedRules.map((item, index) => {
  1939. const key = getTableRuleTargetKey(item, index);
  1940. const label = normalizeFieldMatchText(getTableRuleTargetLabel(item, index));
  1941. const fallbackRule = (key && fallbackRuleMap[key]) || (label && fallbackRuleMap[label]) || null;
  1942. return sanitizeTreeTableRuleInfo({
  1943. ...cloneDeepSafe(fallbackRule || {}),
  1944. ...cloneDeepSafe(item),
  1945. } as TreeTableRuleInfo);
  1946. });
  1947. };
  1948. const mergeStoredFileInfoWithMock = (item: TreeFileInfo): TreeFileInfo => {
  1949. const mockItem = mockFileInfoById.get(normalizeRuleText(item?.id));
  1950. const mergedChildrenSource = Array.isArray(item.children) ? item.children : mockItem?.children;
  1951. return sanitizeTreeFileInfo({
  1952. ...cloneDeepSafe(mockItem || item),
  1953. ...cloneDeepSafe(item),
  1954. filePath: normalizeRuleText(mockItem?.filePath) || item.filePath,
  1955. tableRuleList: mergeStoredTableRuleListWithFallback(item.tableRuleList, mockItem?.tableRuleList),
  1956. children: Array.isArray(mergedChildrenSource)
  1957. ? mergedChildrenSource.map((child) => mergeStoredFileInfoWithMock(child))
  1958. : [],
  1959. });
  1960. };
  1961. const buildMockTemplateList = (list: TreeFileInfo[]): TemplateListItem[] => {
  1962. const templateMap = new Map<number, TemplateListItem>();
  1963. list.forEach((item) => {
  1964. const templateId = normalizeTemplateId(item.templateId);
  1965. if (!templateId || templateMap.has(templateId)) return;
  1966. const tableRules = normalizeTableRuleList(item.tableRuleList);
  1967. const headers = normalizeTemplateHeaders(item.header);
  1968. templateMap.set(templateId, {
  1969. id: templateId,
  1970. tableNameCn: item.tableName || item.sheetName || `模板${templateId}`,
  1971. tableNameEn: `template_${templateId}`,
  1972. classify: inferTemplateClassify(item),
  1973. headers: headers.join(','),
  1974. funcRegx: normalizeRecognitionConfigText(item.funcRegx) || undefined,
  1975. tableFields: tableRules.length
  1976. ? tableRules.map((rule, index) => ({
  1977. fieldName: getTableRuleTargetKey(rule, index),
  1978. fieldType: rule.fieldType || 'string',
  1979. fieldComment: getTableRuleTargetLabel(rule, index),
  1980. required: getTableRuleRequiredFlag(rule) ? 1 : 0,
  1981. matched: getTableRuleMatchedDisplayFlag(rule) ? 1 : 0,
  1982. }))
  1983. : headers.map((header, index) => ({
  1984. fieldName: createTemplateFieldKey(header, index),
  1985. fieldType: 'string',
  1986. fieldComment: header,
  1987. })),
  1988. source: 1,
  1989. });
  1990. });
  1991. return Array.from(templateMap.values());
  1992. };
  1993. const fileList = ref<TreeFileInfo[]>([...mockFileList]);
  1994. const mockTemplateList = buildMockTemplateList(mockFileList);
  1995. const mergeTemplateCollections = (...collections: TemplateListItem[][]) => {
  1996. const templateMap = new Map<string, TemplateListItem>();
  1997. collections.forEach((collection) => {
  1998. collection.forEach((item, index) => {
  1999. const templateId = normalizeTemplateId(item?.id);
  2000. const key = templateId ? String(templateId) : `${item?.tableNameCn || 'template'}_${index}`;
  2001. templateMap.set(key, item);
  2002. });
  2003. });
  2004. return Array.from(templateMap.values());
  2005. };
  2006. const getFileStatusLabel = (status?: string | null) => {
  2007. const normalized = String(status || '').trim();
  2008. if (!normalized) return '待处理';
  2009. return fileStatusLabelMap[normalized] || normalized;
  2010. };
  2011. const getFileStatusVisualState = (status?: string | null) => {
  2012. const normalized = String(status || '').trim();
  2013. if (
  2014. normalized === 'SHEET_TEMPLATE_MATCHED_SUCCESS' ||
  2015. normalized === FileStatusEnum.SHEET_TEMPLATE_MATCHED_SUCCESS
  2016. ) {
  2017. return 'success' as const;
  2018. }
  2019. if (normalized === 'SHEET_TEMPLATE_MATCHED_FAIL' || normalized === FileStatusEnum.SHEET_TEMPLATE_MATCHED_FAIL) {
  2020. return 'fail' as const;
  2021. }
  2022. return null;
  2023. };
  2024. const isTemplateMatchedFailStatus = (status?: string | null) =>
  2025. ['SHEET_TEMPLATE_MATCHED_FAIL', FileStatusEnum.SHEET_TEMPLATE_MATCHED_FAIL].includes(String(status || '').trim());
  2026. const isTreeFileNode = (item?: TreeFileInfo | null): item is TreeFileInfo => !!item && isFileRootItem(item);
  2027. const isTreeSheetNode = (item?: TreeFileInfo | null): item is TreeFileInfo => !!item && !isFileRootItem(item);
  2028. const isTableRuleMatched = (
  2029. item?: TreeTableRuleInfo | null,
  2030. options: { index?: number; amountZfEnabled?: boolean } = {},
  2031. ) => {
  2032. if (!item) return false;
  2033. if (
  2034. options.amountZfEnabled &&
  2035. options.index !== undefined &&
  2036. isLoanFlagTargetField(getTableRuleTargetField(item, options.index))
  2037. ) {
  2038. return true;
  2039. }
  2040. const fileColIndex = Number(item.fileColIndex);
  2041. return (Number.isInteger(fileColIndex) && fileColIndex >= 0) || !!normalizeRuleText(item.fileColName);
  2042. };
  2043. const isMatchSuccessful = (requiredTotal: number, requiredMatched: number, matchedCount: number) => {
  2044. if (requiredTotal > 0) {
  2045. return requiredMatched === requiredTotal;
  2046. }
  2047. return matchedCount > 0;
  2048. };
  2049. const getTableRuleMatchStats = (list?: TreeTableRuleInfo[] | null) => {
  2050. const normalizedRules = normalizeTableRuleList(list);
  2051. const amountZfEnabled = normalizedRules.some(
  2052. (item, index) => isTransAmountTargetField(getTableRuleTargetField(item, index)) && getZfFlag(item.zf),
  2053. );
  2054. const normalizedRuleItems = normalizedRules.map((item, index) => ({ item, index }));
  2055. const requiredRules = normalizedRuleItems.filter(({ item }) => getTableRuleRequiredFlag(item));
  2056. const requiredTotal = requiredRules.length;
  2057. const requiredMatched = requiredRules.filter(({ item, index }) =>
  2058. isTableRuleMatched(item, { index, amountZfEnabled }),
  2059. ).length;
  2060. const matchedCount = normalizedRuleItems.filter(({ item, index }) =>
  2061. isTableRuleMatched(item, { index, amountZfEnabled }),
  2062. ).length;
  2063. return { requiredTotal, requiredMatched, matchedCount };
  2064. };
  2065. const isSheetMatchComplete = (item?: TreeFileInfo | null) => {
  2066. if (!isTreeSheetNode(item)) return false;
  2067. const { requiredTotal, requiredMatched, matchedCount } = getTableRuleMatchStats(item?.tableRuleList);
  2068. return isMatchSuccessful(requiredTotal, requiredMatched, matchedCount);
  2069. };
  2070. const getTreeFileName = (item?: TreeFileInfo | null) => item?.fileName || item?.filePath || '未命名文件';
  2071. const getTreeSheetName = (item?: TreeFileInfo | null) => item?.sheetName || item?.fileName || '未命名工作表';
  2072. const getTreeSheetTitle = (item?: TreeFileInfo | null) =>
  2073. [getTreeSheetName(item), item?.tableName, getFileStatusLabel(item?.fileStatus)].filter(Boolean).join(' / ');
  2074. const getFileNodeTitle = (item: TreeFileInfo) => {
  2075. return isFileRootItem(item) ? getTreeFileName(item) : getTreeSheetName(item);
  2076. };
  2077. const toTreeNode = (item: TreeFileInfo): FileTreeNode => ({
  2078. key: getTreeNodeKey(item),
  2079. title: getFileNodeTitle(item),
  2080. raw: item,
  2081. children: [],
  2082. selectable: !(isFileRootItem(item) && hasChildItems(item)),
  2083. });
  2084. const toTreeNodeDeep = (item: TreeFileInfo): FileTreeNode => {
  2085. const children = (item.children || []).map((child) => toTreeNodeDeep(child));
  2086. const node: FileTreeNode = {
  2087. key: getTreeNodeKey(item),
  2088. title: getFileNodeTitle(item),
  2089. raw: item,
  2090. children,
  2091. selectable: !(isFileRootItem(item) && children.length > 0),
  2092. };
  2093. return node;
  2094. };
  2095. const buildTreeFromFlatList = (list: TreeFileInfo[]) => {
  2096. const keyedList = list.map((item) => ({
  2097. item,
  2098. key: getTreeNodeKey(item),
  2099. }));
  2100. const nodeMap = new Map<string, FileTreeNode>();
  2101. keyedList.forEach(({ item, key }) => {
  2102. nodeMap.set(key, toTreeNode(item));
  2103. });
  2104. const roots: FileTreeNode[] = [];
  2105. keyedList.forEach(({ item, key }) => {
  2106. const node = nodeMap.get(key);
  2107. if (!node) return;
  2108. const parentKey = normalizeParentId(item.pid);
  2109. if (!parentKey) {
  2110. roots.push(node);
  2111. return;
  2112. }
  2113. const parent = nodeMap.get(parentKey);
  2114. if (!parent) {
  2115. roots.push(node);
  2116. return;
  2117. }
  2118. parent.children = parent.children || [];
  2119. parent.children.push(node);
  2120. parent.selectable = false;
  2121. parent.title = getFileNodeTitle({
  2122. ...parent.raw,
  2123. children: parent.children.map((child) => child.raw!).filter(Boolean),
  2124. } as TreeFileInfo);
  2125. });
  2126. return roots;
  2127. };
  2128. const sourceTree = computed<FileTreeNode[]>(() => {
  2129. const list = fileList.value || [];
  2130. if (!list.length) return [];
  2131. // 若后端已返回树结构,则直接按 children 渲染
  2132. const hasNestedChildren = list.some((item) => Array.isArray(item.children) && item.children.length > 0);
  2133. if (hasNestedChildren) {
  2134. return list.map((item) => toTreeNodeDeep(item));
  2135. }
  2136. // 否则按字符串 id/pid 组装树,兼容后端返回的大整数字符串
  2137. return buildTreeFromFlatList(list);
  2138. });
  2139. const sortTreeNodes = (nodes: FileTreeNode[]) => {
  2140. return [...nodes]
  2141. .sort((a, b) => (sortMode.value === 'asc' ? a.title.localeCompare(b.title) : b.title.localeCompare(a.title)))
  2142. .map((node) => ({
  2143. ...node,
  2144. children: node.children?.length ? sortTreeNodes(node.children) : node.children,
  2145. }));
  2146. };
  2147. const treeData = computed(() => {
  2148. const list = sourceTree.value;
  2149. if (!sortMode.value) return list;
  2150. return sortTreeNodes(list);
  2151. });
  2152. const findTreeNodeByKey = (nodes: FileTreeNode[], key: string): FileTreeNode | null => {
  2153. for (const node of nodes) {
  2154. if (node.key === key) return node;
  2155. if (node.children?.length) {
  2156. const matched = findTreeNodeByKey(node.children, key);
  2157. if (matched) return matched;
  2158. }
  2159. }
  2160. return null;
  2161. };
  2162. const findFirstSelectableKey = (nodes: FileTreeNode[]): string => {
  2163. for (const node of nodes) {
  2164. if (node.selectable !== false) {
  2165. return node.key;
  2166. }
  2167. if (node.children?.length) {
  2168. const childKey = findFirstSelectableKey(node.children);
  2169. if (childKey) return childKey;
  2170. }
  2171. }
  2172. return '';
  2173. };
  2174. const resolveSelectableTreeKey = (key: string) => {
  2175. const selectedNode = findTreeNodeByKey(treeData.value, key);
  2176. if (!selectedNode) return '';
  2177. if (selectedNode.selectable !== false) return selectedNode.key;
  2178. return findFirstSelectableKey(selectedNode.children || []);
  2179. };
  2180. const iconMap = {
  2181. unmatched: { key: 'unmatched', label: '未匹配(必选)', color: '#cc3333', icon: '●' },
  2182. matched: { key: 'matched', label: '已匹配', color: '#4ba64b', icon: '●' },
  2183. formatError: { key: 'formatError', label: '格式错误', color: '#f2a100', icon: '!' },
  2184. } as const;
  2185. const iconTips = [iconMap.unmatched, iconMap.matched, iconMap.formatError];
  2186. const activeNodeKey = ref<string>('');
  2187. const selectedTemplateId = ref<number>(0);
  2188. const templateDetail = ref<TableInfo | null>(null);
  2189. const templateDetailFromMock = ref(false);
  2190. const mockTemplateMap = mockTemplateList.reduce(
  2191. (acc, item) => {
  2192. const templateId = normalizeTemplateId(item.id);
  2193. if (templateId) {
  2194. acc[templateId] = item;
  2195. }
  2196. return acc;
  2197. },
  2198. {} as Record<number, TableInfo>,
  2199. );
  2200. const fixedTargetFields = computed(() => {
  2201. const template = templateDetail.value;
  2202. const activeNode = activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
  2203. const tableRules = normalizeTableRuleList(activeNode?.tableRuleList);
  2204. if (!template && !selectedTemplateId.value && !tableRules.length) return [] as MappingTargetField[];
  2205. const baseFields = tableRules.length
  2206. ? buildTargetFieldsFromRuleList(tableRules)
  2207. : buildTargetFieldsFromTemplateInfo(template);
  2208. return applyTableRuleRequiredFlags(baseFields, tableRules);
  2209. });
  2210. const sortedMappingTargetFields = computed(() =>
  2211. fixedTargetFields.value
  2212. .map((item, index) => ({ item, index }))
  2213. .sort((a, b) => {
  2214. const aRequired = isMappingTargetRequired(a.item);
  2215. const bRequired = isMappingTargetRequired(b.item);
  2216. if (aRequired === bRequired) {
  2217. return a.index - b.index;
  2218. }
  2219. return aRequired ? -1 : 1;
  2220. })
  2221. .map(({ item }) => item),
  2222. );
  2223. const defaultRightTableHeaders = ['列A', '列B', '列C', '列D', '列E', '列F', '列G', '列H'];
  2224. const createDefaultSourceRows = () => [
  2225. {
  2226. 列A: '本方号码',
  2227. 列B: '使用人姓名',
  2228. 列C: '号码类别',
  2229. 列D: 'IMSI号',
  2230. 列E: 'IMEI号',
  2231. 列F: '归属运营商标识',
  2232. 列G: '号码归属省',
  2233. 列H: '号码归属市',
  2234. },
  2235. {
  2236. 列A: '13800138000',
  2237. 列B: '张伟',
  2238. 列C: '移动',
  2239. 列D: '460001234567890',
  2240. 列E: '869990012345678',
  2241. 列F: '中国移动',
  2242. 列G: '广东省',
  2243. 列H: '深圳市',
  2244. },
  2245. {
  2246. 列A: '13900139001',
  2247. 列B: '李娜',
  2248. 列C: '联通',
  2249. 列D: '460011112223334',
  2250. 列E: '869990045612345',
  2251. 列F: '中国联通',
  2252. 列G: '北京市',
  2253. 列H: '北京市',
  2254. },
  2255. {
  2256. 列A: '13700137002',
  2257. 列B: '王强',
  2258. 列C: '电信',
  2259. 列D: '460031234500987',
  2260. 列E: '869990078900123',
  2261. 列F: '中国电信',
  2262. 列G: '浙江省',
  2263. 列H: '杭州市',
  2264. },
  2265. {
  2266. 列A: '13600136003',
  2267. 列B: '赵敏',
  2268. 列C: '移动',
  2269. 列D: '460001998877665',
  2270. 列E: '869990011122233',
  2271. 列F: '中国移动',
  2272. 列G: '江苏省',
  2273. 列H: '南京市',
  2274. },
  2275. {
  2276. 列A: '13500135004',
  2277. 列B: '陈涛',
  2278. 列C: '联通',
  2279. 列D: '460019876543210',
  2280. 列E: '869990066677788',
  2281. 列F: '中国联通',
  2282. 列G: '四川省',
  2283. 列H: '成都市',
  2284. },
  2285. {
  2286. 列A: '13400134005',
  2287. 列B: '刘洋',
  2288. 列C: '电信',
  2289. 列D: '460031110000222',
  2290. 列E: '869990099988877',
  2291. 列F: '中国电信',
  2292. 列G: '湖北省',
  2293. 列H: '武汉市',
  2294. },
  2295. {
  2296. 列A: '13300133006',
  2297. 列B: '周倩',
  2298. 列C: '移动',
  2299. 列D: '460001010203040',
  2300. 列E: '869990055544433',
  2301. 列F: '中国移动',
  2302. 列G: '福建省',
  2303. 列H: '厦门市',
  2304. },
  2305. ];
  2306. const rightTableHeaders = ref([...defaultRightTableHeaders]);
  2307. const addedColumns = ref<string[]>([]);
  2308. const selectedAddedColumn = ref('');
  2309. const sourceTableRows = ref<Record<string, string>[]>([]);
  2310. const tablePreviewRows = ref<Record<string, string>[]>(JSON.parse(JSON.stringify(sourceTableRows.value)));
  2311. const rulesList = [{ name: '单列清洗' }, { name: '其他清洗' }];
  2312. type CleaningRuleKey = RulePipelineKey;
  2313. interface CleaningRuleItem {
  2314. name: string;
  2315. key: CleaningRuleKey;
  2316. show: boolean;
  2317. active: boolean;
  2318. }
  2319. interface SelectedRuleItem {
  2320. name: string;
  2321. key: CleaningRuleKey;
  2322. active: boolean;
  2323. enabled: boolean;
  2324. rules: Record<string, any>;
  2325. }
  2326. interface CleaningRuleExample {
  2327. title: string;
  2328. description: string;
  2329. rows: Array<{
  2330. label: string;
  2331. value: string;
  2332. }>;
  2333. }
  2334. interface PreviewColumnSnapshot {
  2335. colIndex: number | null;
  2336. selectedRowIndex: number | null;
  2337. selectedAddedColumn: string;
  2338. activeTargetKey: string;
  2339. }
  2340. type GroupingColumnDraft = {
  2341. header: string;
  2342. direction: string;
  2343. count: number | undefined;
  2344. title: string;
  2345. };
  2346. const ruleNameMap: Record<CleaningRuleKey, string> = {
  2347. ignoreLine: '忽略行',
  2348. dateConfig: '日期和时间',
  2349. extractData: '提取数据',
  2350. extractSegment: '分割提取',
  2351. timestampConversion: '时间戳转换',
  2352. removeSpecialCharacters: '移除特殊字符',
  2353. absoluteValue: '绝对值',
  2354. decimalConversion: '进制转换',
  2355. replaceValue: '条件替换',
  2356. groupingColumns: '组合列',
  2357. magnificationAdjustment: '倍数放大器',
  2358. yearComplete: '年份补全',
  2359. usePreviousRowData: '沿用上一行数据',
  2360. };
  2361. const ruleExampleMap: Partial<Record<CleaningRuleKey, CleaningRuleExample>> = {
  2362. ignoreLine: {
  2363. title: '忽略行',
  2364. description: '按行号跳过无需参与清洗和导入的数据行。',
  2365. rows: [
  2366. { label: '输入', value: '2,5,8' },
  2367. { label: '效果', value: '第 2、5、8 行不会参与后续清洗与导入' },
  2368. ],
  2369. },
  2370. dateConfig: {
  2371. title: '日期和时间',
  2372. description: '识别常见日期或时间文本,并统一转换成标准日期格式。',
  2373. rows: [
  2374. { label: '清洗前', value: '20260324153020' },
  2375. { label: '清洗后', value: '2026-03-24 15:30:20' },
  2376. ],
  2377. },
  2378. extractData: {
  2379. title: '提取数据',
  2380. description: '从字段值前后截取指定长度的内容。',
  2381. rows: [
  2382. { label: '清洗前', value: '13800138000' },
  2383. { label: '前 3 位', value: '138' },
  2384. { label: '后 4 位', value: '8000' },
  2385. ],
  2386. },
  2387. extractSegment: {
  2388. title: '分割提取',
  2389. description: '按分隔符拆分字段值,再提取指定段位。',
  2390. rows: [
  2391. { label: '清洗前', value: '四川省-成都市-高新区' },
  2392. { label: '分隔符', value: '-' },
  2393. { label: '清洗后', value: '成都市' },
  2394. ],
  2395. },
  2396. timestampConversion: {
  2397. title: '时间戳转换',
  2398. description: '自动识别秒级或毫秒级时间戳,并转换为日期时间。',
  2399. rows: [
  2400. { label: '清洗前', value: '1711209600' },
  2401. { label: '清洗后', value: '2024-03-24 00:00:00' },
  2402. ],
  2403. },
  2404. removeSpecialCharacters: {
  2405. title: '移除特殊字符',
  2406. description: '移除空格、换行或自定义特殊字符。',
  2407. rows: [
  2408. { label: '清洗前', value: '138 0013-8000' },
  2409. { label: '清洗后', value: '13800138000' },
  2410. ],
  2411. },
  2412. absoluteValue: {
  2413. title: '绝对值',
  2414. description: '将数值型字段统一转换为正数。',
  2415. rows: [
  2416. { label: '清洗前', value: '-20' },
  2417. { label: '清洗后', value: '20' },
  2418. ],
  2419. },
  2420. decimalConversion: {
  2421. title: '进制转换',
  2422. description: '按配置将数值从当前进制转换到目标进制。',
  2423. rows: [
  2424. { label: '清洗前', value: '56(10进制)' },
  2425. { label: '清洗后', value: '38(16进制)' },
  2426. ],
  2427. },
  2428. replaceValue: {
  2429. title: '条件替换',
  2430. description: '按包含或等于条件,将命中的文本替换为目标值。',
  2431. rows: [
  2432. { label: '包含', value: '未知开户地址 -> --开户地址' },
  2433. { label: '等于', value: '空 -> --' },
  2434. ],
  2435. },
  2436. groupingColumns: {
  2437. title: '组合列',
  2438. description: '将多个来源列按顺序截取并拼接为新列。',
  2439. rows: [
  2440. { label: '配置', value: '姓名前 1 位 + 手机号后 4 位' },
  2441. { label: '清洗后', value: '张2369' },
  2442. ],
  2443. },
  2444. magnificationAdjustment: {
  2445. title: '倍数放大器',
  2446. description: '将数值按配置倍数放大,适合金额或比例字段换算。',
  2447. rows: [
  2448. { label: '清洗前', value: '1.23' },
  2449. { label: '倍数', value: '100' },
  2450. { label: '清洗后', value: '123' },
  2451. ],
  2452. },
  2453. yearComplete: {
  2454. title: '年份补全',
  2455. description: '当日期缺少年份时,按当前年份自动补全。',
  2456. rows: [
  2457. { label: '清洗前', value: '02-23 12:00:00' },
  2458. { label: '清洗后', value: '2026-02-23 12:00:00' },
  2459. ],
  2460. },
  2461. };
  2462. const disabledCleaningRuleKeySet = new Set<CleaningRuleKey>([
  2463. 'groupingColumns',
  2464. 'yearComplete',
  2465. 'removeSpecialCharacters',
  2466. 'usePreviousRowData',
  2467. ]);
  2468. const isCleaningRuleEnabledInUi = (key?: CleaningRuleKey | string | null): key is CleaningRuleKey =>
  2469. !!key && !disabledCleaningRuleKeySet.has(key as CleaningRuleKey);
  2470. const sanitizeSelectedRuleList = (rules?: SelectedRuleItem[] | null) =>
  2471. (Array.isArray(rules) ? rules : []).filter((rule) => isCleaningRuleEnabledInUi(rule?.key));
  2472. const sanitizeSelectedRuleGroups = (groups?: SelectedRuleItem[][] | null) =>
  2473. (Array.isArray(groups) ? groups : []).map((rules) => sanitizeSelectedRuleList(rules));
  2474. const cleaningRulesList = ref<CleaningRuleItem[]>([
  2475. { name: '忽略行', key: 'ignoreLine', show: false, active: false },
  2476. { name: '日期和时间', key: 'dateConfig', show: true, active: false },
  2477. { name: '提取数据', key: 'extractData', show: true, active: false },
  2478. { name: '分割提取', key: 'extractSegment', show: true, active: false },
  2479. { name: '时间戳转换', key: 'timestampConversion', show: true, active: false },
  2480. { name: '绝对值', key: 'absoluteValue', show: true, active: false },
  2481. { name: '进制转换', key: 'decimalConversion', show: true, active: false },
  2482. { name: '条件替换', key: 'replaceValue', show: true, active: false },
  2483. { name: '倍数放大器', key: 'magnificationAdjustment', show: true, active: false },
  2484. ]);
  2485. const activeRules = ref(0);
  2486. const isSharedRuleMode = computed(() => activeRules.value === 1);
  2487. const currentColumn = ref<number | null>(null);
  2488. const activeMappingTargetKey = ref('');
  2489. const selectedRowIndex = ref<number | null>(null);
  2490. const headerRowIndex = ref<number | null>(null);
  2491. const headerRowInput = ref('');
  2492. const tableMockRef = ref<HTMLElement | null>(null);
  2493. const generatedColumnHeaderRowMap = ref<Record<string, number | null>>({});
  2494. const selectRules = ref<SelectedRuleItem[][]>([]);
  2495. const selectedRules = ref<SelectedRuleItem[]>([]);
  2496. const currentRuleTitle = ref('');
  2497. const omitLineInput = ref('');
  2498. const dateConfigType = ref(1);
  2499. const extractDataMode = ref<'before' | 'after'>('before');
  2500. const extractDataBefore = ref('');
  2501. const extractDataAfter = ref('');
  2502. const extractSegmentValue = ref('');
  2503. const extractSegmentRow = ref<number | undefined>(1);
  2504. const absoluteValueChecked = ref(false);
  2505. const decimalEnabled = ref(false);
  2506. const decimalSourceBase = ref('10进制');
  2507. const decimalTargetBase = ref('16进制');
  2508. const replaceEnabled = ref(true);
  2509. const replaceMode = ref(0);
  2510. const replaceSource = ref('');
  2511. const replaceTarget = ref('');
  2512. const removeSpaceChecked = ref(false);
  2513. const removeTabChecked = ref(false);
  2514. const removeCustomChecked = ref(false);
  2515. const removeCustomText = ref('');
  2516. const groupingEnabled = ref(false);
  2517. const groupingColumnsDraft = ref<GroupingColumnDraft[]>(normalizeGroupingColumnsDraft());
  2518. const magnificationValue = ref<number | undefined>(1);
  2519. const syncExtractDataMode = () => {
  2520. extractDataMode.value = extractDataAfter.value && !extractDataBefore.value ? 'after' : 'before';
  2521. };
  2522. const handleExtractDataModeChange = (mode: 'before' | 'after') => {
  2523. extractDataMode.value = mode;
  2524. if (mode === 'before') {
  2525. extractDataAfter.value = '';
  2526. return;
  2527. }
  2528. extractDataBefore.value = '';
  2529. };
  2530. const dateTypeOptions = [
  2531. { label: '日期和时间', value: 1 },
  2532. { label: '日期', value: 2 },
  2533. { label: '时间', value: 3 },
  2534. ];
  2535. const decimalBaseOptions = ['2进制', '8进制', '10进制', '16进制'].map((item) => ({
  2536. label: item,
  2537. value: item,
  2538. }));
  2539. const replaceModeOptions = [
  2540. { label: '包含', value: 0 },
  2541. { label: '等于', value: 1 },
  2542. ];
  2543. function createGroupingColumnDraft(item?: Partial<GroupingColumnDraft> | Record<string, any>): GroupingColumnDraft {
  2544. const header = String(item?.header ?? '').trim();
  2545. const rawCount = item?.count;
  2546. const count =
  2547. rawCount === null || rawCount === undefined || rawCount === ''
  2548. ? undefined
  2549. : Number.isFinite(Number(rawCount))
  2550. ? Number(rawCount)
  2551. : undefined;
  2552. return {
  2553. header,
  2554. direction: String(item?.direction ?? '').trim(),
  2555. count,
  2556. title: String(item?.title ?? header).trim(),
  2557. };
  2558. }
  2559. function normalizeGroupingColumnsDraft(
  2560. list?: Array<Partial<GroupingColumnDraft> | Record<string, any>> | null,
  2561. ): GroupingColumnDraft[] {
  2562. const normalized = Array.isArray(list) ? list.map((item) => createGroupingColumnDraft(item)) : [];
  2563. return normalized.length ? normalized : [createGroupingColumnDraft()];
  2564. }
  2565. type NodeCacheState = {
  2566. selectedTemplateId: number;
  2567. fieldMapping: Record<string, string>;
  2568. rightTableHeaders: string[];
  2569. addedColumns: string[];
  2570. selectedAddedColumn: string;
  2571. sourceTableRows: Record<string, string>[];
  2572. selectRules: SelectedRuleItem[][];
  2573. selectedRules: SelectedRuleItem[];
  2574. activeRules: number;
  2575. currentColumn: number | null;
  2576. selectedRowIndex: number | null;
  2577. headerRowIndex: number | null;
  2578. headerRowInput: string;
  2579. currentRuleTitle: string;
  2580. omitLineInput: string;
  2581. dateConfigType: number;
  2582. extractDataBefore: string;
  2583. extractDataAfter: string;
  2584. extractSegmentValue: string;
  2585. extractSegmentRow: number | undefined;
  2586. absoluteValueChecked: boolean;
  2587. decimalEnabled: boolean;
  2588. decimalSourceBase: string;
  2589. decimalTargetBase: string;
  2590. replaceEnabled: boolean;
  2591. replaceMode: number;
  2592. replaceSource: string;
  2593. replaceTarget: string;
  2594. removeSpaceChecked: boolean;
  2595. removeTabChecked: boolean;
  2596. removeCustomChecked: boolean;
  2597. removeCustomText: string;
  2598. groupingEnabled: boolean;
  2599. groupingColumnsDraft: GroupingColumnDraft[];
  2600. magnificationValue: number | undefined;
  2601. addedColumnConfig: Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>;
  2602. splitColumnConfig: Record<
  2603. string,
  2604. { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
  2605. >;
  2606. generatedColumnHeaderRowMap: Record<string, number | null>;
  2607. recognitionDirectionFields: RecognitionDirectionField[];
  2608. recognitionBasicInfoBaseFuncRegx: string;
  2609. recognitionBasicInfoManualVisible: boolean;
  2610. recognitionBasicInfoExpanded: boolean;
  2611. recognitionBasicInfoRows: RecognitionBasicInfoRow[];
  2612. recognitionAmountZfEnabled: boolean;
  2613. childTemplateName: string;
  2614. };
  2615. const MAX_NODE_CACHE_SIZE = 20;
  2616. const nodeStateCache = new Map<string, NodeCacheState>();
  2617. const initialNodeStatusSignatureMap = new Map<string, string>();
  2618. const analyzingNodeKeys = ref<string[]>([]);
  2619. const cloneDeepSafe = <T,>(val: T): T => JSON.parse(JSON.stringify(val));
  2620. const normalizeGeneratedColumnHeaderRowIndex = (value: unknown, rowCount: number) => {
  2621. const rowIndex = Number(value);
  2622. return Number.isInteger(rowIndex) && rowIndex >= 0 && rowIndex < rowCount ? rowIndex : null;
  2623. };
  2624. const buildGeneratedColumnHeaderRowMap = (
  2625. headers: string[] = addedColumns.value,
  2626. rows: Record<string, string>[] = sourceTableRows.value,
  2627. currentMap?: Record<string, number | null> | null,
  2628. fallbackRowIndex: number | null = headerRowIndex.value,
  2629. ) =>
  2630. headers.reduce(
  2631. (acc, header) => {
  2632. if (!header) return acc;
  2633. if (currentMap && Object.prototype.hasOwnProperty.call(currentMap, header)) {
  2634. acc[header] = normalizeGeneratedColumnHeaderRowIndex(currentMap[header], rows.length);
  2635. return acc;
  2636. }
  2637. const inferredRowIndex = rows.findIndex((row) => String(row?.[header] ?? '') === header);
  2638. acc[header] =
  2639. inferredRowIndex >= 0
  2640. ? inferredRowIndex
  2641. : normalizeGeneratedColumnHeaderRowIndex(fallbackRowIndex, rows.length);
  2642. return acc;
  2643. },
  2644. {} as Record<string, number | null>,
  2645. );
  2646. const getGeneratedColumnHeaderRowIndex = (columnName: string) =>
  2647. buildGeneratedColumnHeaderRowMap(
  2648. addedColumns.value.includes(columnName) ? [columnName] : [],
  2649. sourceTableRows.value,
  2650. generatedColumnHeaderRowMap.value,
  2651. )[columnName] ?? null;
  2652. const setNodeAnalyzing = (nodeKey: string, analyzing: boolean) => {
  2653. if (!nodeKey) return;
  2654. if (analyzing) {
  2655. if (!analyzingNodeKeys.value.includes(nodeKey)) {
  2656. analyzingNodeKeys.value = [...analyzingNodeKeys.value, nodeKey];
  2657. }
  2658. return;
  2659. }
  2660. if (analyzingNodeKeys.value.includes(nodeKey)) {
  2661. analyzingNodeKeys.value = analyzingNodeKeys.value.filter((item) => item !== nodeKey);
  2662. }
  2663. };
  2664. const touchNodeCache = (nodeKey: string, state: NodeCacheState) => {
  2665. if (nodeStateCache.has(nodeKey)) {
  2666. nodeStateCache.delete(nodeKey);
  2667. }
  2668. nodeStateCache.set(nodeKey, state);
  2669. if (nodeStateCache.size > MAX_NODE_CACHE_SIZE) {
  2670. const oldestKey = nodeStateCache.keys().next().value;
  2671. if (oldestKey) nodeStateCache.delete(oldestKey);
  2672. }
  2673. };
  2674. const buildNodeCacheState = (): NodeCacheState => ({
  2675. selectedTemplateId: selectedTemplateId.value,
  2676. fieldMapping: cloneDeepSafe(fieldMapping.value),
  2677. rightTableHeaders: [...rightTableHeaders.value],
  2678. addedColumns: [...addedColumns.value],
  2679. selectedAddedColumn: selectedAddedColumn.value,
  2680. sourceTableRows: cloneDeepSafe(sourceTableRows.value),
  2681. selectRules: sanitizeSelectedRuleGroups(cloneDeepSafe(selectRules.value)),
  2682. selectedRules: sanitizeSelectedRuleList(cloneDeepSafe(selectedRules.value)),
  2683. activeRules: activeRules.value,
  2684. currentColumn: currentColumn.value,
  2685. selectedRowIndex: selectedRowIndex.value,
  2686. headerRowIndex: headerRowIndex.value,
  2687. headerRowInput: headerRowInput.value,
  2688. currentRuleTitle: currentRuleTitle.value,
  2689. omitLineInput: omitLineInput.value,
  2690. dateConfigType: dateConfigType.value,
  2691. extractDataBefore: extractDataBefore.value,
  2692. extractDataAfter: extractDataAfter.value,
  2693. extractSegmentValue: extractSegmentValue.value,
  2694. extractSegmentRow: extractSegmentRow.value,
  2695. absoluteValueChecked: absoluteValueChecked.value,
  2696. decimalEnabled: decimalEnabled.value,
  2697. decimalSourceBase: decimalSourceBase.value,
  2698. decimalTargetBase: decimalTargetBase.value,
  2699. replaceEnabled: replaceEnabled.value,
  2700. replaceMode: replaceMode.value,
  2701. replaceSource: replaceSource.value,
  2702. replaceTarget: replaceTarget.value,
  2703. removeSpaceChecked: removeSpaceChecked.value,
  2704. removeTabChecked: removeTabChecked.value,
  2705. removeCustomChecked: removeCustomChecked.value,
  2706. removeCustomText: removeCustomText.value,
  2707. groupingEnabled: groupingEnabled.value,
  2708. groupingColumnsDraft: cloneDeepSafe(groupingColumnsDraft.value),
  2709. magnificationValue: magnificationValue.value,
  2710. addedColumnConfig: cloneDeepSafe(addedColumnConfig.value),
  2711. splitColumnConfig: cloneDeepSafe(splitColumnConfig.value),
  2712. generatedColumnHeaderRowMap: cloneDeepSafe(buildGeneratedColumnHeaderRowMap()),
  2713. recognitionDirectionFields: cloneDeepSafe(recognitionDirectionFields.value),
  2714. recognitionBasicInfoBaseFuncRegx: recognitionBasicInfoBaseFuncRegx.value,
  2715. recognitionBasicInfoManualVisible: recognitionBasicInfoManualVisible.value,
  2716. recognitionBasicInfoExpanded: recognitionBasicInfoExpanded.value,
  2717. recognitionBasicInfoRows: cloneDeepSafe(recognitionBasicInfoRows.value),
  2718. recognitionAmountZfEnabled: recognitionAmountZfEnabled.value,
  2719. childTemplateName: childTemplateName.value,
  2720. });
  2721. const buildNodeStatusSignature = (state: NodeCacheState) =>
  2722. JSON.stringify({
  2723. selectedTemplateId: state.selectedTemplateId,
  2724. fieldMapping: state.fieldMapping,
  2725. rightTableHeaders: state.rightTableHeaders,
  2726. addedColumns: state.addedColumns,
  2727. headerRowIndex: state.headerRowIndex,
  2728. selectRules: state.selectRules.map((rules) =>
  2729. rules.map((rule) => ({
  2730. key: rule.key,
  2731. name: rule.name,
  2732. enabled: rule.enabled,
  2733. rules: rule.rules,
  2734. })),
  2735. ),
  2736. selectedRules: state.selectedRules.map((rule) => ({
  2737. key: rule.key,
  2738. name: rule.name,
  2739. enabled: rule.enabled,
  2740. rules: rule.rules,
  2741. })),
  2742. addedColumnConfig: state.addedColumnConfig,
  2743. splitColumnConfig: state.splitColumnConfig,
  2744. generatedColumnHeaderRowMap: state.generatedColumnHeaderRowMap,
  2745. recognitionDirectionFields: (state.recognitionDirectionFields || []).map((item) => ({
  2746. key: item.key,
  2747. label: item.label,
  2748. tab: item.tab,
  2749. entries: item.entries.map((entry) => ({
  2750. label: entry.label,
  2751. value: entry.value,
  2752. })),
  2753. })),
  2754. recognitionBasicInfoBaseFuncRegx: state.recognitionBasicInfoBaseFuncRegx,
  2755. recognitionBasicInfoManualVisible: state.recognitionBasicInfoManualVisible,
  2756. recognitionBasicInfoRows: state.recognitionBasicInfoRows.map((item) => ({
  2757. headerMatch: item.headerMatch,
  2758. targetField: item.targetField,
  2759. endFlag: item.endFlag,
  2760. })),
  2761. recognitionAmountZfEnabled: state.recognitionAmountZfEnabled,
  2762. childTemplateName: state.childTemplateName,
  2763. });
  2764. const applyNodeCacheState = (state: NodeCacheState) => {
  2765. selectedTemplateId.value = state.selectedTemplateId;
  2766. fieldMapping.value = cloneDeepSafe(state.fieldMapping);
  2767. rightTableHeaders.value = [...state.rightTableHeaders];
  2768. addedColumns.value = [...state.addedColumns];
  2769. selectedAddedColumn.value = state.selectedAddedColumn;
  2770. sourceTableRows.value = cloneDeepSafe(state.sourceTableRows);
  2771. selectRules.value = sanitizeSelectedRuleGroups(cloneDeepSafe(state.selectRules));
  2772. selectedRules.value = sanitizeSelectedRuleList(cloneDeepSafe(state.selectedRules));
  2773. activeRules.value = state.activeRules;
  2774. currentColumn.value = state.currentColumn;
  2775. selectedRowIndex.value = state.selectedRowIndex;
  2776. headerRowIndex.value = state.headerRowIndex;
  2777. headerRowInput.value = state.headerRowInput;
  2778. currentRuleTitle.value = state.currentRuleTitle;
  2779. omitLineInput.value = state.omitLineInput;
  2780. dateConfigType.value = state.dateConfigType;
  2781. extractDataBefore.value = state.extractDataBefore;
  2782. extractDataAfter.value = state.extractDataAfter;
  2783. syncExtractDataMode();
  2784. extractSegmentValue.value = state.extractSegmentValue;
  2785. extractSegmentRow.value = state.extractSegmentRow;
  2786. absoluteValueChecked.value = state.absoluteValueChecked;
  2787. decimalEnabled.value = state.decimalEnabled;
  2788. decimalSourceBase.value = state.decimalSourceBase;
  2789. decimalTargetBase.value = state.decimalTargetBase;
  2790. replaceEnabled.value = state.replaceEnabled;
  2791. replaceMode.value = state.replaceMode;
  2792. replaceSource.value = state.replaceSource;
  2793. replaceTarget.value = state.replaceTarget;
  2794. removeSpaceChecked.value = state.removeSpaceChecked;
  2795. removeTabChecked.value = state.removeTabChecked;
  2796. removeCustomChecked.value = state.removeCustomChecked;
  2797. removeCustomText.value = state.removeCustomText;
  2798. groupingEnabled.value = state.groupingEnabled;
  2799. groupingColumnsDraft.value = normalizeGroupingColumnsDraft(cloneDeepSafe(state.groupingColumnsDraft));
  2800. magnificationValue.value = state.magnificationValue;
  2801. addedColumnConfig.value = cloneDeepSafe(state.addedColumnConfig);
  2802. splitColumnConfig.value = cloneDeepSafe(state.splitColumnConfig);
  2803. generatedColumnHeaderRowMap.value = buildGeneratedColumnHeaderRowMap(
  2804. state.addedColumns,
  2805. state.sourceTableRows,
  2806. state.generatedColumnHeaderRowMap,
  2807. state.headerRowIndex,
  2808. );
  2809. recognitionDirectionFields.value = cloneDeepSafe(state.recognitionDirectionFields || []);
  2810. recognitionBasicInfoBaseFuncRegx.value = state.recognitionBasicInfoBaseFuncRegx;
  2811. recognitionBasicInfoManualVisible.value = state.recognitionBasicInfoManualVisible;
  2812. recognitionBasicInfoExpanded.value = state.recognitionBasicInfoExpanded;
  2813. recognitionBasicInfoRows.value = cloneDeepSafe(state.recognitionBasicInfoRows);
  2814. recognitionAmountZfEnabled.value = !!state.recognitionAmountZfEnabled;
  2815. childTemplateName.value = state.childTemplateName;
  2816. if (!selectedRules.value.some((item) => item.active) && currentColumn.value !== null) {
  2817. const currentColumnRules = selectRules.value[currentColumn.value] || [];
  2818. if (!currentColumnRules.some((item) => item.active)) {
  2819. currentRuleTitle.value = '';
  2820. }
  2821. }
  2822. runRulePipeline();
  2823. };
  2824. const resetNodeWorkState = () => {
  2825. selectedTemplateId.value = 0;
  2826. templateDetail.value = null;
  2827. templateDetailFromMock.value = false;
  2828. activeTemplateItem.value = null;
  2829. fieldMapping.value = {};
  2830. activeMappingTargetKey.value = '';
  2831. rightTableHeaders.value = [];
  2832. addedColumns.value = [];
  2833. selectedAddedColumn.value = '';
  2834. sourceTableRows.value = [];
  2835. selectRules.value = [];
  2836. selectedRules.value = [];
  2837. activeRules.value = 0;
  2838. currentColumn.value = null;
  2839. selectedRowIndex.value = null;
  2840. headerRowIndex.value = null;
  2841. headerRowInput.value = '';
  2842. currentRuleTitle.value = '';
  2843. omitLineInput.value = '';
  2844. dateConfigType.value = 1;
  2845. extractDataMode.value = 'before';
  2846. extractDataBefore.value = '';
  2847. extractDataAfter.value = '';
  2848. extractSegmentValue.value = '';
  2849. extractSegmentRow.value = 1;
  2850. absoluteValueChecked.value = false;
  2851. decimalEnabled.value = false;
  2852. decimalSourceBase.value = '10进制';
  2853. decimalTargetBase.value = '16进制';
  2854. replaceEnabled.value = true;
  2855. replaceMode.value = 0;
  2856. replaceSource.value = '';
  2857. replaceTarget.value = '';
  2858. removeSpaceChecked.value = false;
  2859. removeTabChecked.value = false;
  2860. removeCustomChecked.value = false;
  2861. removeCustomText.value = '';
  2862. groupingEnabled.value = false;
  2863. groupingColumnsDraft.value = normalizeGroupingColumnsDraft();
  2864. magnificationValue.value = 1;
  2865. addedColumnConfig.value = {};
  2866. splitColumnConfig.value = {};
  2867. generatedColumnHeaderRowMap.value = {};
  2868. recognitionDirectionFields.value = [];
  2869. recognitionBasicInfoBaseFuncRegx.value = '';
  2870. recognitionBasicInfoManualVisible.value = false;
  2871. recognitionBasicInfoExpanded.value = false;
  2872. recognitionBasicInfoRows.value = [];
  2873. recognitionAmountZfEnabled.value = false;
  2874. childTemplateName.value = '';
  2875. runRulePipeline();
  2876. };
  2877. const clearActiveNodeTemplateState = () => {
  2878. const node = activeNodeInfo.value;
  2879. const key = activeNodeKey.value;
  2880. if (!node || !key) return;
  2881. node.templateId = null;
  2882. node.mainId = null;
  2883. node.tableName = null;
  2884. node.tableRuleList = [];
  2885. node.childTemplateName = null;
  2886. node.funcRegx = null;
  2887. nodeStateCache.delete(key);
  2888. };
  2889. const saveCurrentNodeState = () => {
  2890. const key = activeNodeKey.value;
  2891. if (!key) return;
  2892. syncActiveNodeStructure();
  2893. touchNodeCache(key, buildNodeCacheState());
  2894. };
  2895. const switchNodeState = async (nextKey: string) => {
  2896. if (!nextKey) return;
  2897. if (activeNodeKey.value === nextKey) return;
  2898. saveCurrentNodeState();
  2899. setNodeAnalyzing(nextKey, true);
  2900. activeNodeKey.value = nextKey;
  2901. const currentNode = findTreeNodeByKey(treeData.value, nextKey)?.raw || null;
  2902. structureSyncSuspended.value = true;
  2903. try {
  2904. const cached = nodeStateCache.get(nextKey);
  2905. if (cached) {
  2906. applyNodeCacheState(cached);
  2907. await loadTemplateDetail(cached.selectedTemplateId, { silent: true, requestDetail: false });
  2908. touchNodeCache(nextKey, cached);
  2909. return;
  2910. }
  2911. resetNodeWorkState();
  2912. const shouldLoadNodeTemplate = !isTemplateMatchedFailStatus(currentNode?.fileStatus);
  2913. await loadTemplateDetail(shouldLoadNodeTemplate ? currentNode?.templateId : undefined, {
  2914. silent: true,
  2915. requestDetail: false,
  2916. });
  2917. await applyNodePreset(currentNode);
  2918. const initialState = buildNodeCacheState();
  2919. touchNodeCache(nextKey, initialState);
  2920. if (!initialNodeStatusSignatureMap.has(nextKey)) {
  2921. initialNodeStatusSignatureMap.set(nextKey, buildNodeStatusSignature(initialState));
  2922. }
  2923. } finally {
  2924. structureSyncSuspended.value = false;
  2925. syncActiveNodeStructure();
  2926. setNodeAnalyzing(nextKey, false);
  2927. }
  2928. };
  2929. const defaultRuleConfig = (): Record<string, any> => ({
  2930. segment: '',
  2931. segmentRow: 1,
  2932. beforeModel: '',
  2933. nextModel: '',
  2934. timeStamp: false,
  2935. dateType: 1,
  2936. specialList: [],
  2937. specialStr: '',
  2938. absoluteData: false,
  2939. decimalData: false,
  2940. decimalModel: '16进制',
  2941. decimalModel2: '10进制',
  2942. replaceFlag: true,
  2943. sourceData: '',
  2944. targetData: '',
  2945. replaceModel: 0,
  2946. omitLine: '',
  2947. groupFlag: false,
  2948. groupColumns: normalizeGroupingColumnsDraft(),
  2949. magnificationAdjustmentValue: 1,
  2950. });
  2951. const structureSyncSuspended = ref(false);
  2952. const normalizeStoredLineNoToRowIndex = (value?: string | number | null) => {
  2953. const lineNo = Number(value);
  2954. if (!Number.isInteger(lineNo)) return null;
  2955. return lineNo <= 0 ? 0 : lineNo - 1;
  2956. };
  2957. const buildStoredLineNo = (rowIndex: number | null) => {
  2958. if (rowIndex === null || rowIndex < 0) return null;
  2959. return rowIndex + 1;
  2960. };
  2961. const toRuleBoolean = (value: unknown, defaultValue = false) => {
  2962. if (value === null || value === undefined || value === '') {
  2963. return defaultValue;
  2964. }
  2965. if (typeof value === 'string') {
  2966. return value !== 'false';
  2967. }
  2968. return Boolean(value);
  2969. };
  2970. const persistFileListToSessionStorage = () => {
  2971. try {
  2972. sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizeFileInfoTree(fileList.value)));
  2973. } catch (error) {
  2974. // 忽略序列化异常,避免影响当前清洗编辑流程。
  2975. }
  2976. };
  2977. const getElectronIpcRenderer = () => (window as any)?.electron?.ipcRenderer;
  2978. const createPreviewRowsByHeaders = (headers: string[]) => {
  2979. return headers.length ? [] : [];
  2980. };
  2981. const applyPreviewDataset = (dataset: PreviewFileDataset) => {
  2982. rightTableHeaders.value = [...dataset.headers];
  2983. addedColumns.value = [];
  2984. generatedColumnHeaderRowMap.value = {};
  2985. selectedAddedColumn.value = '';
  2986. sourceTableRows.value = dataset.rows.length ? dataset.rows : [];
  2987. tablePreviewRows.value = cloneDeepSafe(sourceTableRows.value);
  2988. return true;
  2989. };
  2990. type PreviewStateSnapshot = PreviewFileDataset & {
  2991. headerRowIndex: number | null;
  2992. selectedRowIndex: number | null;
  2993. };
  2994. const pickPreviewRowByHeaders = (row: Record<string, string>, headers: string[]) =>
  2995. headers.reduce(
  2996. (acc, header) => {
  2997. acc[header] = String(row?.[header] ?? '');
  2998. return acc;
  2999. },
  3000. {} as Record<string, string>,
  3001. );
  3002. const captureSourcePreviewSnapshot = (): PreviewStateSnapshot | null => {
  3003. const addedHeaderSet = new Set(addedColumns.value);
  3004. const headers = rightTableHeaders.value.filter((header) => !addedHeaderSet.has(header));
  3005. if (!headers.length) return null;
  3006. return {
  3007. headers,
  3008. rows: sourceTableRows.value.map((row) => pickPreviewRowByHeaders(row, headers)),
  3009. headerRowIndex: headerRowIndex.value,
  3010. selectedRowIndex: selectedRowIndex.value,
  3011. };
  3012. };
  3013. const restorePreviewSnapshot = (snapshot: PreviewStateSnapshot) => {
  3014. applyPreviewDataset({
  3015. headers: snapshot.headers,
  3016. rows: snapshot.rows,
  3017. });
  3018. headerRowIndex.value = snapshot.headerRowIndex;
  3019. selectedRowIndex.value = snapshot.selectedRowIndex;
  3020. headerRowInput.value = snapshot.headerRowIndex === null ? '' : String(snapshot.headerRowIndex + 1);
  3021. };
  3022. const loadPreviewDatasetFromFile = async (item?: TreeFileInfo | null): Promise<PreviewFileDataset | null> => {
  3023. const normalizedPath = normalizeRuleText(item?.filePath);
  3024. if (!normalizedPath) return null;
  3025. const ipcRenderer = getElectronIpcRenderer();
  3026. if (!ipcRenderer?.invoke) return null;
  3027. try {
  3028. const payload = (await ipcRenderer.invoke('controller/os/readPreviewFile', {
  3029. filePath: normalizedPath,
  3030. headers: normalizeTemplateHeaders(item?.header),
  3031. headerRowIndex: normalizeStoredLineNoToRowIndex(item?.lineNo),
  3032. })) as PreviewFileDataset | null;
  3033. if (!payload?.headers?.length) return null;
  3034. return {
  3035. headers: payload.headers.map((header) => normalizeRuleText(header)).filter(Boolean),
  3036. rows: Array.isArray(payload.rows) ? payload.rows : [],
  3037. };
  3038. } catch (error) {
  3039. console.error('读取预览文件失败', normalizedPath, error);
  3040. return null;
  3041. }
  3042. };
  3043. const applyPresetHeadersFromNode = (item?: TreeFileInfo | null) => {
  3044. const headers = normalizeTemplateHeaders(item?.header);
  3045. if (!headers.length) return false;
  3046. return applyPreviewDataset({
  3047. headers,
  3048. rows: [],
  3049. });
  3050. };
  3051. const insertHeaderAt = (header: string, targetIndex?: number | null) => {
  3052. if (!header) return;
  3053. if (!rightTableHeaders.value.includes(header)) {
  3054. const maxIndex = rightTableHeaders.value.length;
  3055. const nextIndex =
  3056. typeof targetIndex === 'number' && Number.isInteger(targetIndex)
  3057. ? Math.min(Math.max(targetIndex, 0), maxIndex)
  3058. : maxIndex;
  3059. rightTableHeaders.value.splice(nextIndex, 0, header);
  3060. }
  3061. if (!addedColumns.value.includes(header)) {
  3062. addedColumns.value.push(header);
  3063. }
  3064. if (!Object.prototype.hasOwnProperty.call(generatedColumnHeaderRowMap.value, header)) {
  3065. generatedColumnHeaderRowMap.value[header] = headerRowIndex.value;
  3066. }
  3067. sourceTableRows.value = sourceTableRows.value.map((row) => ({
  3068. ...row,
  3069. [header]: String(row[header] ?? ''),
  3070. }));
  3071. };
  3072. const resolveStoredRuleKey = (rule?: TreeStoredFunRule | null) => {
  3073. const directKey = normalizeRuleText(rule?.ruleKey) as CleaningRuleKey;
  3074. if (
  3075. directKey &&
  3076. Object.prototype.hasOwnProperty.call(ruleNameMap, directKey) &&
  3077. isCleaningRuleEnabledInUi(directKey)
  3078. ) {
  3079. return directKey;
  3080. }
  3081. const legacyKey = normalizeRuleText(rule?.key) as CleaningRuleKey;
  3082. if (
  3083. legacyKey &&
  3084. Object.prototype.hasOwnProperty.call(ruleNameMap, legacyKey) &&
  3085. isCleaningRuleEnabledInUi(legacyKey)
  3086. ) {
  3087. return legacyKey;
  3088. }
  3089. const className = normalizeRuleText(rule?.['@class']);
  3090. if (className.endsWith('FunReplace')) return 'replaceValue';
  3091. if (className.endsWith('FunMultiplier')) return 'magnificationAdjustment';
  3092. if (className.endsWith('FunAbs')) return 'absoluteValue';
  3093. if (className.endsWith('FunRadix')) return 'decimalConversion';
  3094. if (className.endsWith('FunSpExtra')) return 'extractSegment';
  3095. if (className.endsWith('FunExtra')) return 'extractData';
  3096. if (className.endsWith('FunDateTime'))
  3097. return legacyKey === 'timestampConversion' ? 'timestampConversion' : 'dateConfig';
  3098. if (className.endsWith('FunDate') || className.endsWith('FunTime')) return 'dateConfig';
  3099. if (className.endsWith('FunRegular')) return 'removeSpecialCharacters';
  3100. return null;
  3101. };
  3102. const deserializeStoredRule = (rule?: TreeStoredFunRule | null): SelectedRuleItem | null => {
  3103. const key = resolveStoredRuleKey(rule);
  3104. if (!key) return null;
  3105. const rules = {
  3106. ...defaultRuleConfig(),
  3107. ...(cloneDeepSafe(rule?.rules || {}) as Record<string, any>),
  3108. };
  3109. const className = normalizeRuleText(rule?.['@class']);
  3110. if (key === 'replaceValue') {
  3111. rules.sourceData = String(rule?.target ?? rules.sourceData ?? '');
  3112. rules.targetData = String(rule?.replacement ?? rules.targetData ?? '');
  3113. }
  3114. if (key === 'magnificationAdjustment') {
  3115. rules.magnificationAdjustmentValue = Number(rule?.factor ?? rules.magnificationAdjustmentValue ?? 1) || 1;
  3116. }
  3117. if (key === 'extractSegment') {
  3118. rules.segment = String(rule?.key ?? rules.segment ?? '');
  3119. rules.segmentRow = Number(rule?.index ?? rules.segmentRow ?? 1) || 1;
  3120. }
  3121. if (key === 'extractData') {
  3122. const len = Number(rule?.len ?? 0) || 0;
  3123. const direction = normalizeRuleText(rule?.metaDirection) || (rule?.opened === false ? 'after' : 'before');
  3124. if (direction === 'after') {
  3125. rules.nextModel = len ? String(len) : rules.nextModel;
  3126. } else {
  3127. rules.beforeModel = len ? String(len) : rules.beforeModel;
  3128. }
  3129. }
  3130. if (key === 'decimalConversion') {
  3131. if (!rules.decimalModel && !rules.decimalModel2) {
  3132. if (Number(rule?.radixType) === 0) {
  3133. rules.decimalModel = '16进制';
  3134. rules.decimalModel2 = '10进制';
  3135. }
  3136. if (Number(rule?.radixType) === 1) {
  3137. rules.decimalModel = '10进制';
  3138. rules.decimalModel2 = '16进制';
  3139. }
  3140. }
  3141. rules.decimalData = rule?.opened ?? rules.decimalData ?? false;
  3142. }
  3143. if (key === 'absoluteValue') {
  3144. rules.absoluteData = rule?.opened ?? rules.absoluteData ?? true;
  3145. }
  3146. if (key === 'dateConfig') {
  3147. if (className.endsWith('FunDate')) {
  3148. rules.dateType = 2;
  3149. } else if (className.endsWith('FunTime')) {
  3150. rules.dateType = 3;
  3151. } else {
  3152. rules.dateType = 1;
  3153. }
  3154. }
  3155. if (key === 'timestampConversion') {
  3156. rules.timeStamp = true;
  3157. }
  3158. if (key === 'removeSpecialCharacters') {
  3159. const regex = String(rule?.regex ?? '');
  3160. if (!Array.isArray(rules.specialList) || !rules.specialList.length) {
  3161. const parsedRegularRule = parseRemoveSpecialRegex(regex);
  3162. rules.specialList = parsedRegularRule.specialList;
  3163. rules.specialStr = parsedRegularRule.specialStr;
  3164. }
  3165. }
  3166. return {
  3167. key,
  3168. name: normalizeRuleText(rule?.name) || ruleNameMap[key],
  3169. active: false,
  3170. enabled: true,
  3171. rules,
  3172. };
  3173. };
  3174. const buildStoredFunRules = (rule: SelectedRuleItem): TreeStoredFunRule[] => {
  3175. const rules = cloneDeepSafe(rule.rules || {});
  3176. const fallbackRule = {
  3177. ruleKey: rule.key,
  3178. rules,
  3179. };
  3180. if (rule.key === 'replaceValue') {
  3181. const fun = new FunReplace();
  3182. fun.target = String(rules.sourceData || '');
  3183. fun.replacement = String(rules.targetData || '');
  3184. return [buildStoredFunPayload(fun)];
  3185. }
  3186. if (rule.key === 'extractSegment') {
  3187. const fun = new FunSpExtra();
  3188. fun.key = String(rules.segment || '');
  3189. fun.index = Number(rules.segmentRow || 1) || 1;
  3190. return [buildStoredFunPayload(fun)];
  3191. }
  3192. if (rule.key === 'extractData') {
  3193. const nextRules: TreeStoredFunRule[] = [];
  3194. const beforeLen = Number(rules.beforeModel || 0) || 0;
  3195. const afterLen = Number(rules.nextModel || 0) || 0;
  3196. if (beforeLen > 0) {
  3197. const fun = new FunExtra();
  3198. fun.opened = true;
  3199. fun.len = beforeLen;
  3200. nextRules.push(buildStoredFunPayload(fun));
  3201. }
  3202. if (afterLen > 0) {
  3203. const fun = new FunExtra();
  3204. fun.opened = false;
  3205. fun.len = afterLen;
  3206. nextRules.push(buildStoredFunPayload(fun));
  3207. }
  3208. return nextRules.length ? nextRules : [fallbackRule];
  3209. }
  3210. if (rule.key === 'timestampConversion') {
  3211. const fun = new FunDateTime();
  3212. return [buildStoredFunPayload(fun, { ruleKey: 'timestampConversion' })];
  3213. }
  3214. if (rule.key === 'dateConfig') {
  3215. const dateType = Number(rules.dateType || 1) || 1;
  3216. const fun = dateType === 2 ? new FunDate() : dateType === 3 ? new FunTime() : new FunDateTime();
  3217. return [buildStoredFunPayload(fun)];
  3218. }
  3219. if (rule.key === 'removeSpecialCharacters') {
  3220. const fun = new FunRegular();
  3221. fun.regex = buildRemoveSpecialRegex(
  3222. Array.isArray(rules.specialList) ? rules.specialList : [],
  3223. String(rules.specialStr || ''),
  3224. );
  3225. return [buildStoredFunPayload(fun)];
  3226. }
  3227. if (rule.key === 'magnificationAdjustment') {
  3228. const fun = new FunMultiplier();
  3229. fun.factor = Number(rules.magnificationAdjustmentValue || 1) || 1;
  3230. return [buildStoredFunPayload(fun)];
  3231. }
  3232. if (rule.key === 'absoluteValue') {
  3233. const fun = new FunAbs();
  3234. fun.opened = toRuleBoolean(rules.absoluteData, true);
  3235. return [buildStoredFunPayload(fun)];
  3236. }
  3237. if (rule.key === 'decimalConversion') {
  3238. const targetBase = normalizeDecimalBaseLabel(rules.decimalModel);
  3239. const currentBase = normalizeDecimalBaseLabel(rules.decimalModel2);
  3240. if (currentBase === '10进制' && targetBase === '16进制') {
  3241. const fun = new FunRadix();
  3242. fun.radixType = 0;
  3243. fun.opened = toRuleBoolean(rules.decimalData, false);
  3244. return [buildStoredFunPayload(fun)];
  3245. }
  3246. if (currentBase === '16进制' && targetBase === '10进制') {
  3247. const fun = new FunRadix();
  3248. fun.radixType = 1;
  3249. fun.opened = toRuleBoolean(rules.decimalData, false);
  3250. return [buildStoredFunPayload(fun)];
  3251. }
  3252. return [fallbackRule];
  3253. }
  3254. return [fallbackRule];
  3255. };
  3256. const serializeStoredFunRules = (rulesList: SelectedRuleItem[], startSort = 0) => {
  3257. let currentSort = startSort;
  3258. return rulesList
  3259. .filter((rule) => rule.enabled && isCleaningRuleEnabledInUi(rule.key))
  3260. .flatMap((rule) =>
  3261. buildStoredFunRules(rule).map((item) => ({
  3262. ...item,
  3263. sort: ++currentSort,
  3264. })),
  3265. );
  3266. };
  3267. const sanitizeStoredFunRulePayload = (
  3268. rule?: TreeStoredFunRule | null,
  3269. options?: { keepRuleKey?: boolean },
  3270. ): TreeStoredFunRule | null => {
  3271. if (!rule) return null;
  3272. const className = normalizeRuleText(rule?.['@class']);
  3273. const sort = Number(rule?.sort);
  3274. const withSort = (payload: TreeStoredFunRule) =>
  3275. Number.isInteger(sort) && sort > 0 ? { ...payload, sort } : payload;
  3276. const keepRuleKey = options?.keepRuleKey !== false;
  3277. if (className.endsWith('FunReplace')) {
  3278. const fun = new FunReplace();
  3279. fun.target = String(rule?.target ?? '');
  3280. fun.replacement = String(rule?.replacement ?? '');
  3281. return withSort(buildStoredFunPayload(fun));
  3282. }
  3283. if (className.endsWith('FunSpExtra')) {
  3284. const fun = new FunSpExtra();
  3285. fun.key = String(rule?.key ?? '');
  3286. fun.index = Number(rule?.index ?? 1) || 1;
  3287. return withSort(buildStoredFunPayload(fun));
  3288. }
  3289. if (className.endsWith('FunExtra')) {
  3290. const fun = new FunExtra();
  3291. fun.opened = rule?.opened !== false;
  3292. fun.len = Number(rule?.len ?? 0) || 0;
  3293. return withSort(buildStoredFunPayload(fun));
  3294. }
  3295. if (className.endsWith('FunDate')) {
  3296. return withSort(buildStoredFunPayload(new FunDate()));
  3297. }
  3298. if (className.endsWith('FunTime')) {
  3299. return withSort(buildStoredFunPayload(new FunTime()));
  3300. }
  3301. if (className.endsWith('FunDateTime')) {
  3302. const payload = buildStoredFunPayload(new FunDateTime());
  3303. if (keepRuleKey && normalizeRuleText(rule?.ruleKey) === 'timestampConversion') {
  3304. return withSort({ ...payload, ruleKey: 'timestampConversion' });
  3305. }
  3306. return withSort(payload);
  3307. }
  3308. if (className.endsWith('FunRegular')) {
  3309. const fun = new FunRegular();
  3310. fun.regex = String(rule?.regex ?? '');
  3311. return withSort(buildStoredFunPayload(fun));
  3312. }
  3313. if (className.endsWith('FunMultiplier')) {
  3314. const fun = new FunMultiplier();
  3315. fun.factor = Number(rule?.factor ?? 1) || 1;
  3316. return withSort(buildStoredFunPayload(fun));
  3317. }
  3318. if (className.endsWith('FunAbs')) {
  3319. const fun = new FunAbs();
  3320. fun.opened = rule?.opened !== false;
  3321. return withSort(buildStoredFunPayload(fun));
  3322. }
  3323. if (className.endsWith('FunRadix')) {
  3324. const fun = new FunRadix();
  3325. fun.radixType = Number(rule?.radixType ?? 1) || 1;
  3326. fun.opened = toRuleBoolean(rule?.opened, false);
  3327. return withSort(buildStoredFunPayload(fun));
  3328. }
  3329. return cloneDeepSafe(rule);
  3330. };
  3331. const sanitizeStoredColumnRulePayload = (rule?: TreeStoredColumnRule | null): TreeStoredColumnRule | null => {
  3332. if (!rule) return null;
  3333. const className = normalizeRuleText(rule?.['@class']);
  3334. const mode = normalizeRuleText(rule?.mode);
  3335. if (className.endsWith('RuleSplit') || mode === 'split') {
  3336. const splitRule = new RuleSplit();
  3337. const refIndex = Number(rule?.refIndex);
  3338. const splitNameIndex = Number(rule?.splitNameIndex);
  3339. const splitNameIndex2 = Number(rule?.splitNameIndex2);
  3340. const len = Number(rule?.len);
  3341. const mappingEnum = Number(rule?.mappingEnum);
  3342. splitRule.refIndex = Number.isInteger(refIndex) && refIndex >= 0 ? refIndex : undefined;
  3343. splitRule.mappingEnum = Number.isInteger(mappingEnum)
  3344. ? mappingEnum
  3345. : Number.isInteger(len) && len > 0
  3346. ? MappingEnum.SP_LEN
  3347. : MappingEnum.SP_CHAR;
  3348. splitRule.splitNameIndex = Number.isInteger(splitNameIndex) && splitNameIndex >= 0 ? splitNameIndex : undefined;
  3349. splitRule.splitNameIndex2 =
  3350. Number.isInteger(splitNameIndex2) && splitNameIndex2 >= 0 ? splitNameIndex2 : undefined;
  3351. splitRule.len = Number.isInteger(len) && len > 0 ? len : undefined;
  3352. splitRule.splitChar = String(rule?.splitChar ?? '') || undefined;
  3353. return buildStoredColumnRulePayload(splitRule);
  3354. }
  3355. if (className.endsWith('RuleMapping') || mode === 'ref' || mode === 'custom') {
  3356. const mappingRule = new RuleMapping();
  3357. mappingRule.refIndex = Array.isArray(rule?.refIndex)
  3358. ? rule.refIndex.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
  3359. : [];
  3360. mappingRule.mappingEnum = Number(rule?.mappingEnum ?? MappingEnum.BLANK);
  3361. mappingRule.content = String(rule?.content ?? '');
  3362. return buildStoredColumnRulePayload(mappingRule);
  3363. }
  3364. return cloneDeepSafe(rule);
  3365. };
  3366. const sanitizeTreeTableRuleInfo = (
  3367. rule: TreeTableRuleInfo,
  3368. options?: { keepRuleKey?: boolean },
  3369. ): TreeTableRuleInfo => {
  3370. return {
  3371. ...cloneDeepSafe(rule),
  3372. funList: Array.isArray(rule.funList)
  3373. ? rule.funList
  3374. .map((item) => sanitizeStoredFunRulePayload(item, options))
  3375. .filter((item): item is TreeStoredFunRule => Boolean(item))
  3376. : rule.funList,
  3377. columnRule: sanitizeStoredColumnRulePayload(rule.columnRule),
  3378. };
  3379. };
  3380. const sanitizeTreeFileInfo = (item: TreeFileInfo, options?: { keepRuleKey?: boolean }): TreeFileInfo => ({
  3381. ...cloneDeepSafe(item),
  3382. tableRuleList: Array.isArray(item.tableRuleList)
  3383. ? item.tableRuleList.map((rule) => sanitizeTreeTableRuleInfo(rule, options))
  3384. : item.tableRuleList,
  3385. children: Array.isArray(item.children)
  3386. ? item.children.map((child) => sanitizeTreeFileInfo(child, options))
  3387. : item.children,
  3388. });
  3389. const sanitizeFileInfoTree = (list: TreeFileInfo[], options?: { keepRuleKey?: boolean }) =>
  3390. list.map((item) => sanitizeTreeFileInfo(item, options));
  3391. const buildColumnRulePayload = (header: string): TreeStoredColumnRule | null => {
  3392. const splitConfig = splitColumnConfig.value[header];
  3393. if (splitConfig) {
  3394. const splitNameIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName1);
  3395. const splitNameIndex2 = rightTableHeaders.value.findIndex((item) => item === splitConfig.targetName2);
  3396. const splitMode = normalizeSplitColumnMode(splitConfig.delimiterType);
  3397. const splitValue = normalizeSplitColumnValue(splitConfig.delimiterType, splitConfig.delimiterValue);
  3398. const splitLength = resolveSplitColumnLength(splitConfig.delimiterType, splitConfig.delimiterValue);
  3399. const splitRule = new RuleSplit();
  3400. splitRule.refIndex = rightTableHeaders.value.findIndex((item) => item === splitConfig.source);
  3401. splitRule.mappingEnum = splitMode === 'length' ? MappingEnum.SP_LEN : MappingEnum.SP_CHAR;
  3402. splitRule.splitNameIndex = splitNameIndex >= 0 ? splitNameIndex : undefined;
  3403. splitRule.splitNameIndex2 = splitNameIndex2 >= 0 ? splitNameIndex2 : undefined;
  3404. splitRule.len = splitMode === 'length' && splitLength > 0 ? splitLength : undefined;
  3405. splitRule.splitChar = splitMode === 'special' && splitValue ? splitValue : undefined;
  3406. return buildStoredColumnRulePayload(splitRule);
  3407. }
  3408. const addedConfig = addedColumnConfig.value[header];
  3409. if (!addedConfig) return null;
  3410. const mappingRule = new RuleMapping();
  3411. mappingRule.refIndex = addedConfig.contentFields
  3412. .map((field) => rightTableHeaders.value.findIndex((item) => item === field))
  3413. .filter((index) => index >= 0);
  3414. mappingRule.mappingEnum =
  3415. addedConfig.mode === 'custom'
  3416. ? addedConfig.customContent
  3417. ? MappingEnum.NEW_DATA
  3418. : MappingEnum.BLANK
  3419. : mappingRule.refIndex.length
  3420. ? MappingEnum.REF
  3421. : MappingEnum.BLANK;
  3422. mappingRule.content = addedConfig.mode === 'custom' ? addedConfig.customContent || '' : '';
  3423. return buildStoredColumnRulePayload(mappingRule);
  3424. };
  3425. const restoreStoredColumnRules = (list?: TreeTableRuleInfo[] | null) => {
  3426. const headerSnapshot = [...rightTableHeaders.value];
  3427. const normalizedRules = normalizeTableRuleList(list);
  3428. const splitRuleGroups = normalizedRules.reduce((acc, rule) => {
  3429. const header = normalizeRuleText(rule.fileColName);
  3430. const columnRule = rule.columnRule;
  3431. const className = normalizeRuleText(columnRule?.['@class']);
  3432. const mode = normalizeRuleText(columnRule?.mode);
  3433. if (!header || (!className.endsWith('RuleSplit') && mode !== 'split')) {
  3434. return acc;
  3435. }
  3436. const fileColIndex = Number(rule.fileColIndex);
  3437. const identity = getSplitColumnRuleIdentity(columnRule);
  3438. const prev = acc.get(identity) || [];
  3439. prev.push({
  3440. header,
  3441. fileColIndex: Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : Number.MAX_SAFE_INTEGER,
  3442. });
  3443. acc.set(identity, prev);
  3444. return acc;
  3445. }, new Map<string, Array<{ header: string; fileColIndex: number }>>());
  3446. normalizedRules.forEach((rule) => {
  3447. const header = normalizeRuleText(rule.fileColName);
  3448. if (!header) return;
  3449. const columnRule = rule.columnRule;
  3450. const fileColIndex = Number(rule.fileColIndex);
  3451. const targetIndex = Number.isInteger(fileColIndex) && fileColIndex >= 0 ? fileColIndex : undefined;
  3452. if (columnRule) {
  3453. insertHeaderAt(header, targetIndex);
  3454. }
  3455. const className = normalizeRuleText(columnRule?.['@class']);
  3456. const mode = normalizeRuleText(columnRule?.mode);
  3457. if (className.endsWith('RuleSplit') || mode === 'split') {
  3458. const refIndex = Number(columnRule?.refIndex);
  3459. const source =
  3460. Number.isInteger(refIndex) && refIndex >= 0
  3461. ? headerSnapshot[refIndex] || rightTableHeaders.value[refIndex] || ''
  3462. : '';
  3463. if (!source) return;
  3464. const splitChar = String(columnRule?.splitChar ?? '');
  3465. const splitLength = Number(columnRule?.len);
  3466. const splitMappingEnum = Number(columnRule?.mappingEnum);
  3467. const isLengthSplit =
  3468. splitMappingEnum === MappingEnum.SP_LEN ||
  3469. (!Number.isInteger(splitMappingEnum) && Number.isInteger(splitLength) && splitLength > 0);
  3470. const splitNameIndex = Number(columnRule?.splitNameIndex);
  3471. const splitNameIndex2 = Number(columnRule?.splitNameIndex2);
  3472. const groupedTargetItems = (splitRuleGroups.get(getSplitColumnRuleIdentity(columnRule)) || [])
  3473. .slice()
  3474. .sort((a, b) => a.fileColIndex - b.fileColIndex);
  3475. const groupedTargets = groupedTargetItems.map((item) => item.header).filter(Boolean);
  3476. const uniqueTargets = Array.from(new Set(groupedTargets));
  3477. const resolveGroupedTargetByIndex = (index: number) =>
  3478. Number.isInteger(index) && index >= 0
  3479. ? groupedTargetItems.find((item) => item.fileColIndex === index)?.header || ''
  3480. : '';
  3481. let targetName1 = resolveGroupedTargetByIndex(splitNameIndex);
  3482. let targetName2 = resolveGroupedTargetByIndex(splitNameIndex2);
  3483. if (!targetName1 && !targetName2) {
  3484. targetName1 = uniqueTargets[0] || header;
  3485. targetName2 = uniqueTargets.find((item) => item !== targetName1) || '';
  3486. }
  3487. if (targetName1 && targetName2 && targetName1 === targetName2) {
  3488. if (Number.isInteger(fileColIndex) && fileColIndex === splitNameIndex2) {
  3489. targetName1 = '';
  3490. } else {
  3491. targetName2 = '';
  3492. }
  3493. }
  3494. splitColumnConfig.value[header] = {
  3495. source,
  3496. delimiterType: isLengthSplit ? 'length' : 'special',
  3497. delimiterValue: isLengthSplit ? String(splitLength > 0 ? splitLength : '') : splitChar,
  3498. targetName1,
  3499. targetName2,
  3500. };
  3501. return;
  3502. }
  3503. if (className.endsWith('RuleMapping') || mode === 'ref' || mode === 'custom') {
  3504. const refIndexList = Array.isArray(columnRule?.refIndex)
  3505. ? columnRule.refIndex.map((item) => Number(item)).filter((item) => Number.isInteger(item) && item >= 0)
  3506. : [];
  3507. const mappingEnum = Number(columnRule?.mappingEnum);
  3508. const isRefMapping = mappingEnum === MappingEnum.REF || (!Number.isFinite(mappingEnum) && mode === 'ref');
  3509. addedColumnConfig.value[header] = {
  3510. mode: isRefMapping ? 'ref' : 'custom',
  3511. contentFields: refIndexList
  3512. .map((index) => headerSnapshot[index] || rightTableHeaders.value[index])
  3513. .filter(Boolean),
  3514. customContent: String(columnRule?.content ?? ''),
  3515. };
  3516. }
  3517. });
  3518. };
  3519. const restoreStoredFunRules = (list?: TreeTableRuleInfo[] | null) => {
  3520. selectedRules.value = [];
  3521. selectRules.value = rightTableHeaders.value.map(() => []);
  3522. const sharedRuleKeySet = new Set<string>();
  3523. const mergeColumnRule = (
  3524. columnRules: SelectedRuleItem[],
  3525. nextRule: SelectedRuleItem,
  3526. rawRule: TreeStoredFunRule,
  3527. ) => {
  3528. if (nextRule.key !== 'extractData') {
  3529. columnRules.push(nextRule);
  3530. return;
  3531. }
  3532. const lastRule = columnRules[columnRules.length - 1];
  3533. if (!lastRule || lastRule.key !== 'extractData') {
  3534. columnRules.push(nextRule);
  3535. return;
  3536. }
  3537. const direction = normalizeRuleText(rawRule?.metaDirection) || (rawRule?.opened === false ? 'after' : 'before');
  3538. if (direction === 'after') {
  3539. lastRule.rules.nextModel = nextRule.rules.nextModel;
  3540. } else {
  3541. lastRule.rules.beforeModel = nextRule.rules.beforeModel;
  3542. }
  3543. };
  3544. normalizeTableRuleList(list).forEach((tableRule) => {
  3545. const header = normalizeRuleText(tableRule.fileColName);
  3546. const columnIndex = header ? rightTableHeaders.value.findIndex((item) => item === header) : -1;
  3547. const columnRules: SelectedRuleItem[] = [];
  3548. (tableRule.funList || [])
  3549. .slice()
  3550. .sort((a, b) => Number(a?.sort ?? 0) - Number(b?.sort ?? 0))
  3551. .forEach((rule) => {
  3552. const nextRule = deserializeStoredRule(rule);
  3553. if (!nextRule) return;
  3554. if (nextRule.key === 'ignoreLine') {
  3555. if (!sharedRuleKeySet.has(nextRule.key)) {
  3556. selectedRules.value.push(nextRule);
  3557. sharedRuleKeySet.add(nextRule.key);
  3558. }
  3559. return;
  3560. }
  3561. mergeColumnRule(columnRules, nextRule, rule);
  3562. });
  3563. if (columnIndex >= 0 && columnRules.length) {
  3564. selectRules.value[columnIndex] = sanitizeSelectedRuleList(columnRules);
  3565. }
  3566. });
  3567. selectedRules.value = sanitizeSelectedRuleList(selectedRules.value);
  3568. };
  3569. const restoreFileLevelRules = (item?: TreeFileInfo | null) => {
  3570. const ignoreLines = Array.isArray(item?.ignoreLines)
  3571. ? item.ignoreLines.map((line) => Number(line)).filter((line) => Number.isInteger(line) && line > 0)
  3572. : [];
  3573. if (ignoreLines.length) {
  3574. const existingIgnoreRule = selectedRules.value.find((rule) => rule.key === 'ignoreLine');
  3575. const nextIgnoreRules = {
  3576. ...defaultRuleConfig(),
  3577. ...(existingIgnoreRule?.rules || {}),
  3578. omitLine: ignoreLines.join(','),
  3579. };
  3580. if (existingIgnoreRule) {
  3581. existingIgnoreRule.rules = nextIgnoreRules;
  3582. existingIgnoreRule.enabled = true;
  3583. } else {
  3584. selectedRules.value = [
  3585. ...selectedRules.value,
  3586. {
  3587. name: ruleNameMap.ignoreLine,
  3588. key: 'ignoreLine',
  3589. active: selectedRules.value.length === 0,
  3590. enabled: true,
  3591. rules: nextIgnoreRules,
  3592. },
  3593. ];
  3594. }
  3595. }
  3596. };
  3597. const resolveTemplateFieldMeta = (field: MappingTargetField, index: number) => {
  3598. const tableFields = getOrderedTemplateFields(
  3599. templateDetail.value?.tableFields || activeTemplateItem.value?.tableFields,
  3600. );
  3601. return (
  3602. tableFields.find(
  3603. (item, itemIndex) =>
  3604. normalizeRuleText(getTemplateFieldTargetKey(item, itemIndex)) === field.key ||
  3605. normalizeRuleText(getTemplateFieldTargetLabel(item, itemIndex)) === field.label,
  3606. ) || null
  3607. );
  3608. };
  3609. const findTemplateFieldMetaByField = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
  3610. const tableFields = getOrderedTemplateFields(
  3611. templateDetail.value?.tableFields || activeTemplateItem.value?.tableFields,
  3612. );
  3613. return (
  3614. tableFields.find(
  3615. (item, index) =>
  3616. normalizeRuleText(getTemplateFieldTargetKey(item, index)) === field.key ||
  3617. normalizeRuleText(getTemplateFieldTargetLabel(item, index)) === normalizeRuleText(field.label),
  3618. ) || null
  3619. );
  3620. };
  3621. const buildTableRuleListFromState = (list?: TreeTableRuleInfo[] | null) => {
  3622. const previousRules = normalizeTableRuleList(list);
  3623. const currentTemplateTableId = normalizeTemplateId(
  3624. templateDetail.value?.id ??
  3625. activeTemplateItem.value?.id ??
  3626. selectedTemplateId.value ??
  3627. activeNodeInfo.value?.templateId,
  3628. );
  3629. const previousRuleMap = previousRules.reduce(
  3630. (acc, item, index) => {
  3631. const key = getTableRuleTargetKey(item, index);
  3632. if (key) acc[key] = item;
  3633. return acc;
  3634. },
  3635. {} as Record<string, TreeTableRuleInfo>,
  3636. );
  3637. const sharedFunRules = serializeStoredFunRules(selectedRules.value);
  3638. const directionConfMap = recognitionDirectionFields.value.reduce(
  3639. (acc, item) => {
  3640. acc[item.key] = buildRecognitionDirectionConf(item.entries);
  3641. return acc;
  3642. },
  3643. {} as Record<string, string>,
  3644. );
  3645. return fixedTargetFields.value.map((field, index) => {
  3646. const previous = previousRuleMap[field.key];
  3647. const requiredMarker = getTableRuleRequiredFlag(previous, field.required) || field.required;
  3648. const mappedHeader = normalizeRuleText(effectiveFieldMapping.value[field.key]);
  3649. const matchedByRecognitionRule = isTargetHandledByRecognitionRule(field.key);
  3650. const zf = isTransAmountTargetField(field) ? (recognitionAmountZfEnabled.value ? 1 : 0) : undefined;
  3651. const fileColIndex = mappedHeader ? rightTableHeaders.value.findIndex((item) => item === mappedHeader) : -1;
  3652. const columnFunRules =
  3653. fileColIndex >= 0 ? serializeStoredFunRules(selectRules.value[fileColIndex] || [], sharedFunRules.length) : [];
  3654. const templateField = resolveTemplateFieldMeta(field, index);
  3655. const templateFieldRecord = (templateField || {}) as Record<string, any>;
  3656. const requiredFlagValue = pickFirstDefinedRuleFlagValue(
  3657. getRawRuleFlagValue(templateFieldRecord, 'required'),
  3658. getRawRuleFlagValue(previous, 'required'),
  3659. requiredMarker ? 1 : 0,
  3660. );
  3661. const matchedFlagValue =
  3662. matchedByRecognitionRule || mappedHeader
  3663. ? 1
  3664. : pickFirstDefinedRuleFlagValue(
  3665. getRawRuleFlagValue(templateFieldRecord, 'matched'),
  3666. getRawRuleFlagValue(previous, 'matched'),
  3667. requiredMarker ? 1 : 0,
  3668. );
  3669. const matchedRequiredFlagValue = pickFirstDefinedRuleFlagValue(getRawRuleFlagValue(previous, 'matchedRequired'));
  3670. const directionConf =
  3671. directionConfMap[field.key] ||
  3672. normalizeRecognitionConfigText(previous?.directionConf) ||
  3673. normalizeRecognitionConfigText(templateFieldRecord.directionConf ?? templateFieldRecord.direction_conf);
  3674. const fileRuleList = [...(index === 0 ? sharedFunRules : []), ...columnFunRules];
  3675. return {
  3676. id: previous?.id ?? null,
  3677. tableId: currentTemplateTableId || normalizeTemplateId(previous?.tableId) || null,
  3678. fieldNameCn: field.label,
  3679. fieldNameEn: field.key,
  3680. fieldType: normalizeRuleText(templateField?.fieldType) || normalizeRuleText(previous?.fieldType) || 'string',
  3681. required: requiredFlagValue,
  3682. fieldLen: previous?.fieldLen ?? null,
  3683. matched: matchedFlagValue,
  3684. matchedRequired: matchedRequiredFlagValue,
  3685. directionConf: directionConf || null,
  3686. zf,
  3687. fieldSort: index + 1,
  3688. fileColIndex: fileColIndex >= 0 ? fileColIndex : null,
  3689. fileColName: mappedHeader || null,
  3690. funList: fileRuleList,
  3691. columnRule: mappedHeader ? buildColumnRulePayload(mappedHeader) : null,
  3692. } as TreeTableRuleInfo;
  3693. });
  3694. };
  3695. const syncNodeStructure = (node?: TreeFileInfo | null) => {
  3696. if (!isTreeSheetNode(node)) return;
  3697. const templateId = normalizeTemplateId(
  3698. templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value ?? node.templateId,
  3699. );
  3700. const mainId = getSelectedTemplateMainId(node.mainId);
  3701. const tableName =
  3702. normalizeRuleText(templateDetail.value?.tableNameCn) ||
  3703. normalizeRuleText(activeTemplateItem.value?.tableNameCn) ||
  3704. normalizeRuleText(node.tableName);
  3705. const baseHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
  3706. const existingHeaders = normalizeTemplateHeaders(node.header);
  3707. const hasRealHeaderConfig =
  3708. headerRowIndex.value !== null ||
  3709. !!existingHeaders.length ||
  3710. addedColumns.value.length > 0 ||
  3711. Object.values(effectiveFieldMapping.value).some(Boolean) ||
  3712. baseHeaders.length > 0;
  3713. const shouldPersistRecognitionBasicInfo = hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value);
  3714. const ignoreLines = Array.from(ignoredRowsSet.value).sort((a, b) => a - b);
  3715. const groupColumns = buildEnabledGroupingColumnsPayload();
  3716. node.templateId = templateId || node.templateId || null;
  3717. node.mainId = mainId || null;
  3718. node.tableName = tableName || node.tableName || null;
  3719. if (headerRowIndex.value !== null) {
  3720. node.lineNo = buildStoredLineNo(headerRowIndex.value);
  3721. }
  3722. if (hasRealHeaderConfig) {
  3723. node.header = baseHeaders.length ? [...baseHeaders] : [];
  3724. }
  3725. node.ignoreLines = ignoreLines;
  3726. node.groupColumns = groupColumns;
  3727. node.tableRuleList = buildTableRuleListFromState(node.tableRuleList);
  3728. node.childTemplateName = normalizeRuleText(childTemplateName.value) || null;
  3729. node.hasNew = !!node.childTemplateName;
  3730. if (shouldPersistRecognitionBasicInfo) {
  3731. node.funcRegx = buildRecognitionBasicInfoFuncRegx();
  3732. } else if (Object.prototype.hasOwnProperty.call(node, 'funcRegx')) {
  3733. node.funcRegx = null;
  3734. }
  3735. persistFileListToSessionStorage();
  3736. };
  3737. const syncActiveNodeStructure = () => {
  3738. if (structureSyncSuspended.value) return;
  3739. syncNodeStructure(activeNodeInfo.value);
  3740. };
  3741. const visibleCleaningRules = computed(() => cleaningRulesList.value.filter((item) => item.show));
  3742. const currentSelectedRules = computed(() => {
  3743. if (isSharedRuleMode.value) {
  3744. return sanitizeSelectedRuleList(selectedRules.value);
  3745. }
  3746. if (currentColumn.value === null) return [];
  3747. return sanitizeSelectedRuleList(selectRules.value[currentColumn.value] || []);
  3748. });
  3749. const currentActiveSelectedRule = computed(() => currentSelectedRules.value.find((item) => item.active));
  3750. const currentPickedRule = computed(() => visibleCleaningRules.value.find((item) => item.active));
  3751. const currentPickedRuleKey = computed<CleaningRuleKey | undefined>(
  3752. () => currentActiveSelectedRule.value?.key || currentPickedRule.value?.key,
  3753. );
  3754. const currentRuleExample = computed(() =>
  3755. currentPickedRuleKey.value ? ruleExampleMap[currentPickedRuleKey.value] : undefined,
  3756. );
  3757. const currentActiveRuleDisplayName = computed(() =>
  3758. currentPickedRuleKey.value ? ruleNameMap[currentPickedRuleKey.value] : '等待选择',
  3759. );
  3760. const getIgnoredRowsSetFromSelectedRules = (rules: SelectedRuleItem[] = []) => {
  3761. const activeIgnoreRule = rules.find((rule) => rule.key === 'ignoreLine' && rule.enabled);
  3762. const raw = String(activeIgnoreRule?.rules?.omitLine || '').trim();
  3763. const nums = raw
  3764. .split(',')
  3765. .map((s) => Number(s.trim()))
  3766. .filter((n) => Number.isInteger(n) && n > 0);
  3767. return new Set(nums);
  3768. };
  3769. const ignoredRowsSet = computed(() => getIgnoredRowsSetFromSelectedRules(selectedRules.value));
  3770. const currentColumnTitle = computed(() => {
  3771. if (currentColumn.value === null) return '';
  3772. return rightTableHeaders.value[currentColumn.value] || '';
  3773. });
  3774. const currentRuleEnabledLabel = computed(() => (currentActiveSelectedRule.value?.enabled ? '已启用' : '未启用'));
  3775. const currentRuleEnabledClass = computed(() =>
  3776. currentActiveSelectedRule.value?.enabled ? 'is-enabled' : 'is-disabled',
  3777. );
  3778. const currentRuleScopeType = computed(() => (isSharedRuleMode.value ? 'shared' : 'column'));
  3779. const currentRuleScopeLabel = computed(() => (currentRuleScopeType.value === 'shared' ? '共享规则' : '列级规则'));
  3780. const groupingHeaderOptions = computed(() =>
  3781. rightTableHeaders.value.map((header) => ({
  3782. label: header,
  3783. value: header,
  3784. })),
  3785. );
  3786. const isCurrentColumnAdded = computed(
  3787. () => !!currentColumnTitle.value && addedColumns.value.includes(currentColumnTitle.value),
  3788. );
  3789. const activeNodeTableRuleMap = computed(() =>
  3790. normalizeTableRuleList(findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw?.tableRuleList).reduce(
  3791. (acc, item, index) => {
  3792. const key = getTableRuleTargetKey(item, index);
  3793. if (key) {
  3794. acc[key] = item;
  3795. }
  3796. return acc;
  3797. },
  3798. {} as Record<string, TreeTableRuleInfo>,
  3799. ),
  3800. );
  3801. const findActiveNodeTableRule = (field: Pick<MappingTargetField, 'key' | 'label'>) => {
  3802. const ruleByKey = activeNodeTableRuleMap.value[field.key];
  3803. if (ruleByKey) {
  3804. return ruleByKey;
  3805. }
  3806. const normalizedLabel = normalizeRuleText(field.label);
  3807. if (!normalizedLabel) {
  3808. return null;
  3809. }
  3810. return (
  3811. normalizeTableRuleList(findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw?.tableRuleList).find(
  3812. (item, index) => {
  3813. const ruleLabel = normalizeRuleText(getTableRuleTargetLabel(item, index));
  3814. return ruleLabel === normalizedLabel;
  3815. },
  3816. ) || null
  3817. );
  3818. };
  3819. const isMappingTargetRequired = (field: Pick<MappingTargetField, 'key' | 'label' | 'required'>) => {
  3820. const templateField = findTemplateFieldMetaByField(field);
  3821. return getTableRuleRequiredFlag(
  3822. findActiveNodeTableRule(field),
  3823. getTemplateFieldRequiredFlag(templateField, field.required),
  3824. );
  3825. };
  3826. const formatTypeLabelMap: Record<FormatValidationType, string> = {
  3827. number: '数值',
  3828. date: '日期',
  3829. dateTime: '日期时间',
  3830. time: '时间',
  3831. loanFlag: '借贷标志',
  3832. callType: '主被叫',
  3833. phone: '号码',
  3834. acctCardNo: '账号',
  3835. idCardNumber: '身份证号',
  3836. };
  3837. const normalizeFormatValidationType = (fieldType?: string | null, fieldLabel = ''): FormatValidationType | '' => {
  3838. const normalizedFieldType = normalizeRuleText(fieldType)
  3839. .replace(/[\s-]+/g, '_')
  3840. .toUpperCase();
  3841. if (['TEXT', 'STRING', 'CHAR', 'VARCHAR', 'LONGTEXT', 'CLOB', '1'].includes(normalizedFieldType)) {
  3842. return '';
  3843. }
  3844. if (['DATE_TIME', 'DATETIME', 'TIMESTAMP', '3'].includes(normalizedFieldType)) {
  3845. return 'dateTime';
  3846. }
  3847. if (['DATE', 'DATE_ONLY', 'DATEONLY', '2'].includes(normalizedFieldType)) {
  3848. return 'date';
  3849. }
  3850. if (['TIME', '6'].includes(normalizedFieldType)) {
  3851. return 'time';
  3852. }
  3853. if (['PHONE', 'MOBILE', 'TEL', 'TELEPHONE'].includes(normalizedFieldType)) {
  3854. return 'phone';
  3855. }
  3856. if (
  3857. [
  3858. 'ACCT_CARD_NO',
  3859. 'ACCOUNT',
  3860. 'ACCOUNT_NO',
  3861. 'ACCOUNT_NUMBER',
  3862. 'ACCT_NO',
  3863. 'CARD_NO',
  3864. 'BANK_CARD',
  3865. 'BANK_CARD_NO',
  3866. ].includes(normalizedFieldType)
  3867. ) {
  3868. return 'acctCardNo';
  3869. }
  3870. if (['DEBIT_CREDIT_FLAG', 'LOAN_FLAG'].includes(normalizedFieldType)) {
  3871. return 'loanFlag';
  3872. }
  3873. if (['CALL_DIRECTION', 'CALL_TYPE'].includes(normalizedFieldType)) {
  3874. return 'callType';
  3875. }
  3876. if (['ID_CARD_NUMBER', 'ID_CARD', 'IDENTITY_CARD', 'CERT_NO', 'CERTIFICATE_NO'].includes(normalizedFieldType)) {
  3877. return 'idCardNumber';
  3878. }
  3879. if (
  3880. [
  3881. 'NUMBER',
  3882. 'NUMERIC',
  3883. 'DECIMAL',
  3884. 'DOUBLE',
  3885. 'FLOAT',
  3886. 'INTEGER',
  3887. 'INT',
  3888. 'LONG',
  3889. 'BIGINT',
  3890. 'BIGDECIMAL',
  3891. 'AMOUNT',
  3892. 'MONEY',
  3893. '4',
  3894. '5',
  3895. ].includes(normalizedFieldType)
  3896. ) {
  3897. return 'number';
  3898. }
  3899. if (normalizedFieldType) {
  3900. return '';
  3901. }
  3902. const normalizedLabel = normalizeFieldMatchText(fieldLabel);
  3903. if (!normalizedLabel) return '';
  3904. if (/(号码状态|手机状态|手机号状态|电话状态)/.test(normalizedLabel)) {
  3905. return '';
  3906. }
  3907. if (
  3908. /(电话|联系电话|手机|手机号|手机号码|电话号码|本方号码|对方号码|主叫号码|被叫号码|号码)/.test(normalizedLabel) &&
  3909. !/(类别|类型|归属地|地区|地址|编号|运单号|证件号|卡号|账号)/.test(normalizedLabel)
  3910. ) {
  3911. return 'phone';
  3912. }
  3913. if (
  3914. /(金额|数值|数量|次数|时长|单价|价格|余额|积分|额度|里程|公里|重量|身高|年龄|税额|总价)/.test(normalizedLabel)
  3915. ) {
  3916. return 'number';
  3917. }
  3918. if (normalizedLabel.includes('日期时间')) {
  3919. return 'dateTime';
  3920. }
  3921. if ((normalizedLabel.includes('日期') && normalizedLabel.includes('时间')) || normalizedLabel.includes('时刻')) {
  3922. return 'dateTime';
  3923. }
  3924. if (normalizedLabel.includes('时间')) {
  3925. return 'dateTime';
  3926. }
  3927. if (normalizedLabel.includes('日期') || normalizedLabel.includes('生日') || normalizedLabel.includes('出生')) {
  3928. return 'date';
  3929. }
  3930. return '';
  3931. };
  3932. const isLeapYear = (year: number) => (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  3933. const getMonthDayCount = (year: number, month: number) =>
  3934. [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] || 0;
  3935. const isValidDateParts = (year: number, month: number, day: number) =>
  3936. year >= 1000 && month >= 1 && month <= 12 && day >= 1 && day <= getMonthDayCount(year, month);
  3937. const isValidTimeParts = (hour: number, minute: number, second = 0, fraction = '') =>
  3938. hour >= 0 &&
  3939. hour <= 23 &&
  3940. minute >= 0 &&
  3941. minute <= 59 &&
  3942. second >= 0 &&
  3943. second <= 59 &&
  3944. (!fraction || /^\d{1,6}$/u.test(fraction));
  3945. const normalizeFormatValue = (value: unknown) =>
  3946. String(value ?? '')
  3947. .replace(/\u00a0/g, ' ')
  3948. .trim();
  3949. const normalizeDateLikeText = (value: string) => {
  3950. const normalized = normalizeFormatValue(value)
  3951. .replace(/(Z|[+-]\d{2}:?\d{2})$/iu, '')
  3952. .replace(/年/g, '-')
  3953. .replace(/月/g, '-')
  3954. .replace(/[日号]/g, ' ')
  3955. .replace(/[Tt]/g, ' ')
  3956. .replace(/\//g, '-')
  3957. .replace(/^(\d{4})\.(\d{1,2})\.(\d{1,2})(.*)$/u, '$1-$2-$3$4')
  3958. .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')
  3959. .replace(/[点时]/g, ':')
  3960. .replace(/分/g, ':')
  3961. .replace(/秒/g, '')
  3962. .replace(/\s+/g, ' ')
  3963. .trim();
  3964. return normalized.endsWith(':') ? normalized.slice(0, -1) : normalized;
  3965. };
  3966. const isValidDateOnlyValue = (value: string) => {
  3967. const normalized = normalizeDateLikeText(value);
  3968. const dashedMatch = normalized.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/u);
  3969. if (dashedMatch) {
  3970. return isValidDateParts(Number(dashedMatch[1]), Number(dashedMatch[2]), Number(dashedMatch[3]));
  3971. }
  3972. const compactMatch = normalized.match(/^(\d{4})(\d{2})(\d{2})$/u);
  3973. if (compactMatch) {
  3974. return isValidDateParts(Number(compactMatch[1]), Number(compactMatch[2]), Number(compactMatch[3]));
  3975. }
  3976. return false;
  3977. };
  3978. const isValidTimeOnlyValue = (value: string) => {
  3979. const normalized = normalizeDateLikeText(value);
  3980. const matched = normalized.match(/^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:[:.](\d{1,6}))?$/u);
  3981. if (!matched) return false;
  3982. return isValidTimeParts(Number(matched[1]), Number(matched[2]), Number(matched[3] || 0), matched[4] || '');
  3983. };
  3984. const isValidDateTimeValue = (value: string) => {
  3985. const normalized = normalizeDateLikeText(value);
  3986. if (isValidDateOnlyValue(normalized) || isValidTimeOnlyValue(normalized)) {
  3987. return true;
  3988. }
  3989. const dashedMatch = normalized.match(
  3990. /^(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?:[:.](\d{1,6}))?$/u,
  3991. );
  3992. if (dashedMatch) {
  3993. return (
  3994. isValidDateParts(Number(dashedMatch[1]), Number(dashedMatch[2]), Number(dashedMatch[3])) &&
  3995. isValidTimeParts(
  3996. Number(dashedMatch[4]),
  3997. Number(dashedMatch[5]),
  3998. Number(dashedMatch[6] || 0),
  3999. dashedMatch[7] || '',
  4000. )
  4001. );
  4002. }
  4003. const compactMatch = normalized.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{1,6})?$/u);
  4004. if (compactMatch) {
  4005. return (
  4006. isValidDateParts(Number(compactMatch[1]), Number(compactMatch[2]), Number(compactMatch[3])) &&
  4007. isValidTimeParts(
  4008. Number(compactMatch[4]),
  4009. Number(compactMatch[5]),
  4010. Number(compactMatch[6]),
  4011. compactMatch[7] || '',
  4012. )
  4013. );
  4014. }
  4015. return false;
  4016. };
  4017. const isValidNumberValue = (value: string) => {
  4018. const normalized = normalizeFormatValue(value).replace(/[,\s,¥¥]/g, '');
  4019. return /^[-+]?(?:\d+\.?\d*|\.\d+)$/u.test(normalized);
  4020. };
  4021. const isValidPhoneValue = (value: string) => {
  4022. const normalized = normalizeFormatValue(value);
  4023. if (!normalized) return true;
  4024. if (/^(未知|空号|无|暂无)$/u.test(normalized)) {
  4025. return true;
  4026. }
  4027. const compactValue = normalized.replace(/[\s\-()()+]/g, '');
  4028. const digitOnlyValue = compactValue.replace(/\D/g, '');
  4029. if (digitOnlyValue.length < 5) {
  4030. return false;
  4031. }
  4032. if (/^[\d*#]+$/u.test(compactValue)) {
  4033. return true;
  4034. }
  4035. return /^(\+?\d[\d\s\-()()]{4,}|\d{5,})$/u.test(normalized);
  4036. };
  4037. const isValidAcctCardNoValue = (value: string) => {
  4038. const normalized = normalizeFormatValue(value);
  4039. if (!normalized) return true;
  4040. const compactValue = normalized.replace(/[\s\-()()]/g, '');
  4041. if (!compactValue) return true;
  4042. return /^[A-Za-z0-9*#]{6,32}$/u.test(compactValue);
  4043. };
  4044. const getIdCardCheckCode = (body: string) => {
  4045. const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
  4046. const codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
  4047. const total = body.split('').reduce((sum, char, index) => sum + Number(char) * weights[index], 0);
  4048. return codes[total % 11];
  4049. };
  4050. const isValidIdCardNumberValue = (value: string) => {
  4051. const normalized = normalizeFormatValue(value);
  4052. if (!normalized) return true;
  4053. const compactValue = normalized.replace(/\s/g, '').toUpperCase();
  4054. if (/^\d{15}$/u.test(compactValue)) {
  4055. const year = Number(`19${compactValue.slice(6, 8)}`);
  4056. const month = Number(compactValue.slice(8, 10));
  4057. const day = Number(compactValue.slice(10, 12));
  4058. return isValidDateParts(year, month, day);
  4059. }
  4060. if (!/^\d{17}[\dX]$/u.test(compactValue)) {
  4061. return false;
  4062. }
  4063. const year = Number(compactValue.slice(6, 10));
  4064. const month = Number(compactValue.slice(10, 12));
  4065. const day = Number(compactValue.slice(12, 14));
  4066. if (!isValidDateParts(year, month, day)) {
  4067. return false;
  4068. }
  4069. return getIdCardCheckCode(compactValue.slice(0, 17)) === compactValue[17];
  4070. };
  4071. const loanFlagCoreValueMap: Record<string, string> = {
  4072. 收入: '收入',
  4073. 支出: '支出',
  4074. 借方: '收入',
  4075. 贷方: '支出',
  4076. 借: '收入',
  4077. 贷: '支出',
  4078. };
  4079. const normalizeLoanFlagCoreValue = (value: string) => {
  4080. const normalized = normalizeFormatValue(value).replace(/[\s_\-()()/]/g, '');
  4081. if (!normalized) return '';
  4082. return loanFlagCoreValueMap[normalized] || '';
  4083. };
  4084. const callTypeCoreValueMap: Record<string, string> = {
  4085. 主叫: '主叫',
  4086. 被叫: '被叫',
  4087. 发送: '发送',
  4088. 接收: '接收',
  4089. 呼入: '呼入',
  4090. 呼出: '呼出',
  4091. 上行: '上行',
  4092. 下行: '下行',
  4093. 来电: '呼入',
  4094. 去电: '呼出',
  4095. CALLIN: '被叫',
  4096. CALLOUT: '主叫',
  4097. SMSIN: '接收',
  4098. SMSOUT: '发送',
  4099. };
  4100. const normalizeCallTypeCoreValue = (value: string) => {
  4101. const normalized = normalizeFormatValue(value)
  4102. .toUpperCase()
  4103. .replace(/[\s_\-()()/]/g, '');
  4104. if (!normalized) return '';
  4105. if (callTypeCoreValueMap[normalized]) {
  4106. return callTypeCoreValueMap[normalized];
  4107. }
  4108. const simplified = normalized.replace(/短信|彩信|通话|话单|记录|类型|类别|方向/g, '');
  4109. return callTypeCoreValueMap[simplified] || '';
  4110. };
  4111. const isValidLoanFlagValue = (value: string, targetKey = '') => {
  4112. const normalized = normalizeFormatValue(value);
  4113. if (!normalized) return true;
  4114. const matchedByConfig = matchRecognitionDirectionConfigValue(normalized, '借贷标志', targetKey);
  4115. if (matchedByConfig !== null) return matchedByConfig;
  4116. return !!normalizeLoanFlagCoreValue(normalized);
  4117. };
  4118. const isValidCallTypeValue = (value: string, targetKey = '') => {
  4119. const normalized = normalizeFormatValue(value);
  4120. if (!normalized) return true;
  4121. const matchedByConfig = matchRecognitionDirectionConfigValue(normalized, '通信类别', targetKey);
  4122. if (matchedByConfig !== null) return matchedByConfig;
  4123. return !!normalizeCallTypeCoreValue(normalized);
  4124. };
  4125. const isValueValidByFormatType = (value: string, formatType: FormatValidationType, targetKey = '') => {
  4126. switch (formatType) {
  4127. case 'number':
  4128. return isValidNumberValue(value);
  4129. case 'date':
  4130. return isValidDateOnlyValue(value);
  4131. case 'dateTime':
  4132. return isValidDateTimeValue(value);
  4133. case 'time':
  4134. return isValidTimeOnlyValue(value);
  4135. case 'loanFlag':
  4136. return isValidLoanFlagValue(value, targetKey);
  4137. case 'callType':
  4138. return isValidCallTypeValue(value, targetKey);
  4139. case 'phone':
  4140. return isValidPhoneValue(value);
  4141. case 'acctCardNo':
  4142. return isValidAcctCardNoValue(value);
  4143. case 'idCardNumber':
  4144. return isValidIdCardNumberValue(value);
  4145. default:
  4146. return true;
  4147. }
  4148. };
  4149. const targetFormatMetaMap = computed(() =>
  4150. fixedTargetFields.value.reduce(
  4151. (acc, field, index) => {
  4152. const templateFieldRecord = (resolveTemplateFieldMeta(field, index) || {}) as Record<string, any>;
  4153. const rawFieldType = normalizeRuleText(
  4154. templateFieldRecord.fieldType ??
  4155. templateFieldRecord.field_type ??
  4156. activeNodeTableRuleMap.value[field.key]?.fieldType,
  4157. );
  4158. acc[field.key] = {
  4159. key: field.key,
  4160. label: field.label,
  4161. formatType: normalizeFormatValidationType(rawFieldType, field.label),
  4162. };
  4163. return acc;
  4164. },
  4165. {} as Record<string, { key: string; label: string; formatType: FormatValidationType | '' }>,
  4166. ),
  4167. );
  4168. const matchedTargetFieldsByHeader = computed(() => {
  4169. return Object.entries(effectiveFieldMapping.value).reduce(
  4170. (acc, [targetKey, header]) => {
  4171. const mappedHeader = normalizeRuleText(header);
  4172. if (!mappedHeader || !acc[mappedHeader]) return acc;
  4173. const targetMeta = targetFormatMetaMap.value[targetKey];
  4174. if (targetMeta) {
  4175. acc[mappedHeader].push(targetMeta);
  4176. }
  4177. return acc;
  4178. },
  4179. rightTableHeaders.value.reduce(
  4180. (acc, header) => {
  4181. acc[header] = [];
  4182. return acc;
  4183. },
  4184. {} as Record<string, Array<{ key: string; label: string; formatType: FormatValidationType | '' }>>,
  4185. ),
  4186. );
  4187. });
  4188. const previewNormalizationTargetMetaByHeader = computed(() =>
  4189. rightTableHeaders.value.reduce(
  4190. (acc, header) => {
  4191. const matchedTarget = (matchedTargetFieldsByHeader.value[header] || []).find((item) =>
  4192. ['loanFlag', 'callType'].includes(item.formatType),
  4193. );
  4194. if (matchedTarget?.formatType) {
  4195. acc[header] = matchedTarget as {
  4196. key: string;
  4197. label: string;
  4198. formatType: 'loanFlag' | 'callType';
  4199. };
  4200. }
  4201. return acc;
  4202. },
  4203. {} as Record<string, { key: string; label: string; formatType: 'loanFlag' | 'callType' }>,
  4204. ),
  4205. );
  4206. const resolveRecognitionDirectionMatchedGroup = (
  4207. value: string,
  4208. tab: RecognitionRuleTabKey,
  4209. targetKey = '',
  4210. entries?: RecognitionDirectionItem[],
  4211. ) => {
  4212. const normalizedValue = normalizeRecognitionConfigText(value);
  4213. if (!normalizedValue) return '';
  4214. const fieldEntries =
  4215. entries ||
  4216. recognitionDirectionFields.value.find((item) => item.key === targetKey && item.tab === tab)?.entries ||
  4217. [];
  4218. const matchedEntry = fieldEntries.find((entry) => {
  4219. const comparableTexts = [entry.label, ...splitRecognitionDirectionKeywords(entry.value)];
  4220. return comparableTexts.some((keyword) => {
  4221. const normalizedKeyword = normalizeRecognitionConfigText(keyword);
  4222. return !!normalizedKeyword && normalizedKeyword === normalizedValue;
  4223. });
  4224. });
  4225. if (matchedEntry) {
  4226. return (
  4227. normalizeRecognitionDirectionGroup(matchedEntry.label, tab) ||
  4228. normalizeRecognitionConfigText(matchedEntry.label)
  4229. );
  4230. }
  4231. return '';
  4232. };
  4233. const normalizePreviewFormatValue = (
  4234. value: string,
  4235. targetMeta?: { key: string; label: string; formatType: 'loanFlag' | 'callType' },
  4236. ) => {
  4237. const normalizedValue = String(value ?? '');
  4238. if (!targetMeta || !normalizedValue) return normalizedValue;
  4239. if (targetMeta.formatType === 'loanFlag') {
  4240. return resolveRecognitionDirectionMatchedGroup(normalizedValue, '借贷标志', targetMeta.key) || normalizedValue;
  4241. }
  4242. return resolveRecognitionDirectionMatchedGroup(normalizedValue, '通信类别', targetMeta.key) || normalizedValue;
  4243. };
  4244. const normalizePreviewRowsByTargetFormat = (rows: Record<string, string>[]) =>
  4245. rows.map((row, rowIndex) => {
  4246. if (headerRowIndex.value === null || rowIndex <= headerRowIndex.value || ignoredRowsSet.value.has(rowIndex + 1)) {
  4247. return row;
  4248. }
  4249. let nextRow: Record<string, string> | null = null;
  4250. Object.entries(previewNormalizationTargetMetaByHeader.value).forEach(([header, targetMeta]) => {
  4251. const normalizedCellValue = normalizePreviewFormatValue(String(row?.[header] ?? ''), targetMeta);
  4252. if (normalizedCellValue === String(row?.[header] ?? '')) return;
  4253. if (!nextRow) {
  4254. nextRow = { ...row };
  4255. }
  4256. nextRow[header] = normalizedCellValue;
  4257. });
  4258. return nextRow || row;
  4259. });
  4260. const formatValidationRows = computed(() => {
  4261. const currentHeaderRowIndex = headerRowIndex.value;
  4262. if (currentHeaderRowIndex === null) {
  4263. return [] as Record<string, string>[];
  4264. }
  4265. return tablePreviewRows.value.filter((_row, rowIndex) => {
  4266. const lineNumber = rowIndex + 1;
  4267. if (ignoredRowsSet.value.has(lineNumber)) return false;
  4268. return rowIndex > currentHeaderRowIndex;
  4269. });
  4270. });
  4271. const buildFormatValidationTitle = (invalidTargets: HeaderFormatValidationState['invalidTargets']) =>
  4272. invalidTargets
  4273. .map(({ label, formatType, invalidValue }) => {
  4274. const shortValue = invalidValue.length > 18 ? `${invalidValue.slice(0, 18)}...` : invalidValue;
  4275. return `${label}需符合${formatTypeLabelMap[formatType]}格式,当前检测到“${shortValue}”`;
  4276. })
  4277. .join(';');
  4278. const shouldSkipFormatValidationByTarget = (targetMeta: {
  4279. key: string;
  4280. label: string;
  4281. formatType: FormatValidationType;
  4282. }) => {
  4283. const normalizedLabel = normalizeFieldMatchText(targetMeta.label);
  4284. return normalizedLabel === '号码归属省' || normalizedLabel === '号码归属市';
  4285. };
  4286. const getHeaderFormatValidationCandidates = (header: string) =>
  4287. Array.from(
  4288. (matchedTargetFieldsByHeader.value[header] || [])
  4289. .reduce((candidateMap, targetMeta) => {
  4290. if (!targetMeta.formatType) return candidateMap;
  4291. if (
  4292. shouldSkipFormatValidationByTarget(
  4293. targetMeta as {
  4294. key: string;
  4295. label: string;
  4296. formatType: FormatValidationType;
  4297. },
  4298. )
  4299. ) {
  4300. return candidateMap;
  4301. }
  4302. candidateMap.set(`${targetMeta.key}:${targetMeta.formatType}`, {
  4303. key: targetMeta.key,
  4304. label: targetMeta.label,
  4305. formatType: targetMeta.formatType,
  4306. });
  4307. return candidateMap;
  4308. }, new Map<string, { key: string; label: string; formatType: FormatValidationType }>())
  4309. .values(),
  4310. );
  4311. const headerFormatValidationMap = computed(() =>
  4312. rightTableHeaders.value.reduce(
  4313. (acc, header) => {
  4314. const invalidTargets = getHeaderFormatValidationCandidates(header).reduce(
  4315. (list, targetMeta) => {
  4316. const formatType = targetMeta.formatType;
  4317. const invalidValue =
  4318. formatValidationRows.value.find((row) => {
  4319. const cellValue = normalizeFormatValue(row[header]);
  4320. return !!cellValue && !isValueValidByFormatType(cellValue, formatType, targetMeta.key);
  4321. })?.[header] || '';
  4322. const normalizedInvalidValue = normalizeFormatValue(invalidValue);
  4323. if (!normalizedInvalidValue) return list;
  4324. list.push({
  4325. key: targetMeta.key,
  4326. label: targetMeta.label,
  4327. formatType,
  4328. invalidValue: normalizedInvalidValue,
  4329. });
  4330. return list;
  4331. },
  4332. [] as HeaderFormatValidationState['invalidTargets'],
  4333. );
  4334. acc[header] = {
  4335. invalidTargets,
  4336. title: buildFormatValidationTitle(invalidTargets),
  4337. };
  4338. return acc;
  4339. },
  4340. {} as Record<string, HeaderFormatValidationState>,
  4341. ),
  4342. );
  4343. const hasHeaderFormatError = (header: string) => !!headerFormatValidationMap.value[header]?.invalidTargets.length;
  4344. const getHeaderFormatErrorTitle = (header: string) => headerFormatValidationMap.value[header]?.title || '';
  4345. const getTargetFormatErrorState = (targetKey: string) => {
  4346. const mappedHeader = effectiveFieldMapping.value[targetKey];
  4347. if (!mappedHeader) return null;
  4348. return headerFormatValidationMap.value[mappedHeader]?.invalidTargets.find((item) => item.key === targetKey) || null;
  4349. };
  4350. const hasTargetFormatError = (targetKey: string) => !!getTargetFormatErrorState(targetKey);
  4351. const getTargetFormatErrorTitle = (targetKey: string) => {
  4352. const matchedState = getTargetFormatErrorState(targetKey);
  4353. if (!matchedState) return '';
  4354. return buildFormatValidationTitle([matchedState]);
  4355. };
  4356. const getMatchedTargetFieldText = (header: string) =>
  4357. matchedTargetFieldsByHeader.value[header]?.map((item) => item.label).join('、') || '';
  4358. const hasMatchedTargetField = (header: string) => !!matchedTargetFieldsByHeader.value[header]?.length;
  4359. const getMatchedHeaderStatusClass = (header: string) => (hasMatchedTargetField(header) ? 'is-success' : 'is-fail');
  4360. const getMatchedHeaderStatusText = (header: string) => (hasMatchedTargetField(header) ? '✓' : '✕');
  4361. const getMatchedHeaderStatusTitle = (header: string) => (hasMatchedTargetField(header) ? '已匹配' : '未匹配');
  4362. const normalizeHeaderLabel = (value: string | number | null | undefined, index: number) => {
  4363. const normalized = String(value ?? '')
  4364. .trim()
  4365. .replace(/\s+/g, ' ');
  4366. return normalized || `未命名列${index + 1}`;
  4367. };
  4368. const buildHeaderLabelsByRow = (
  4369. previousHeaders: string[],
  4370. row: Record<string, string>,
  4371. fixedHeaders: string[] = [],
  4372. ) => {
  4373. const fixedHeaderSet = new Set(fixedHeaders);
  4374. const usedLabels = new Set(fixedHeaders);
  4375. return previousHeaders.map((header, index) => {
  4376. if (fixedHeaderSet.has(header)) {
  4377. return header;
  4378. }
  4379. const baseName = normalizeHeaderLabel(row[header], index);
  4380. if (!usedLabels.has(baseName)) {
  4381. usedLabels.add(baseName);
  4382. return baseName;
  4383. }
  4384. let suffix = 2;
  4385. let candidate = `${baseName}_${suffix}`;
  4386. while (usedLabels.has(candidate)) {
  4387. suffix += 1;
  4388. candidate = `${baseName}_${suffix}`;
  4389. }
  4390. usedLabels.add(candidate);
  4391. return candidate;
  4392. });
  4393. };
  4394. const renameRecordKeys = <T,>(record: Record<string, T>, keyMap: Record<string, string>) => {
  4395. return Object.entries(record).reduce(
  4396. (acc, [key, value]) => {
  4397. acc[keyMap[key] || key] = value;
  4398. return acc;
  4399. },
  4400. {} as Record<string, T>,
  4401. );
  4402. };
  4403. const buildStoredFunPayload = <T extends object>(
  4404. payload: T,
  4405. extras?: Partial<TreeStoredFunRule>,
  4406. ): TreeStoredFunRule => ({
  4407. ...(cloneDeepSafe({ ...payload }) as TreeStoredFunRule),
  4408. ...(extras || {}),
  4409. });
  4410. const buildStoredColumnRulePayload = <T extends object>(payload: T): TreeStoredColumnRule =>
  4411. cloneDeepSafe({ ...payload }) as TreeStoredColumnRule;
  4412. const splitRegexTokens = (value: string) => {
  4413. const tokens: string[] = [];
  4414. let current = '';
  4415. let escaped = false;
  4416. for (const char of value) {
  4417. if (!escaped && char === '|') {
  4418. tokens.push(current);
  4419. current = '';
  4420. continue;
  4421. }
  4422. current += char;
  4423. escaped = !escaped && char === '\\';
  4424. if (char !== '\\') {
  4425. escaped = false;
  4426. }
  4427. }
  4428. if (current) {
  4429. tokens.push(current);
  4430. }
  4431. return tokens;
  4432. };
  4433. const parseRemoveSpecialRegex = (regex: string) => {
  4434. const normalized = String(regex || '').trim();
  4435. if (!normalized) {
  4436. return { specialList: [] as string[], specialStr: '' };
  4437. }
  4438. const body =
  4439. normalized.startsWith('(?:') && normalized.endsWith(')')
  4440. ? normalized.slice(3, normalized.length - 1)
  4441. : normalized;
  4442. const tokens = splitRegexTokens(body);
  4443. const specialList: string[] = [];
  4444. const customTokens: string[] = [];
  4445. tokens.forEach((token) => {
  4446. if (!token) return;
  4447. if (token === '\\s') {
  4448. specialList.push('移除空格字符');
  4449. return;
  4450. }
  4451. if (token === '\\r\\n') {
  4452. specialList.push('移除换行字符(tab)');
  4453. return;
  4454. }
  4455. customTokens.push(token.replace(/\\(.)/g, '$1'));
  4456. });
  4457. if (customTokens.length) {
  4458. specialList.push('要移除的特殊字符');
  4459. }
  4460. return {
  4461. specialList,
  4462. specialStr: customTokens.join('|'),
  4463. };
  4464. };
  4465. const getSplitColumnRuleIdentity = (rule?: TreeStoredColumnRule | null) => {
  4466. const refIndex = Number(rule?.refIndex);
  4467. const splitNameIndex = Number(rule?.splitNameIndex);
  4468. const splitNameIndex2 = Number(rule?.splitNameIndex2);
  4469. const len = Number(rule?.len);
  4470. const splitChar = String(rule?.splitChar ?? '');
  4471. return JSON.stringify({
  4472. refIndex: Number.isInteger(refIndex) ? refIndex : null,
  4473. mappingEnum: Number(rule?.mappingEnum ?? MappingEnum.SP_CHAR),
  4474. splitNameIndex: Number.isInteger(splitNameIndex) ? splitNameIndex : null,
  4475. splitNameIndex2: Number.isInteger(splitNameIndex2) ? splitNameIndex2 : null,
  4476. len: Number.isInteger(len) && len > 0 ? len : null,
  4477. splitChar,
  4478. });
  4479. };
  4480. const remapGroupingColumnRules = (
  4481. list: Array<Partial<GroupingColumnDraft> | Record<string, any>> | undefined,
  4482. keyMap: Record<string, string>,
  4483. removedHeaders: Set<string> = new Set(),
  4484. ) =>
  4485. normalizeGroupingColumnsDraft(list).map((item) => {
  4486. const nextHeader = removedHeaders.has(item.header) ? '' : keyMap[item.header] || item.header;
  4487. return createGroupingColumnDraft({
  4488. ...item,
  4489. header: nextHeader,
  4490. title: nextHeader || '',
  4491. });
  4492. });
  4493. const remapSelectRuleHeaderRefs = (
  4494. groups: SelectedRuleItem[][],
  4495. keyMap: Record<string, string>,
  4496. removedHeaders: Set<string> = new Set(),
  4497. ) =>
  4498. groups.map((rules) =>
  4499. rules.map((rule) => {
  4500. if (rule.key !== 'groupingColumns') {
  4501. return rule;
  4502. }
  4503. return {
  4504. ...rule,
  4505. rules: {
  4506. ...rule.rules,
  4507. groupColumns: remapGroupingColumnRules(rule.rules?.groupColumns, keyMap, removedHeaders),
  4508. },
  4509. };
  4510. }),
  4511. );
  4512. const syncHeaderRowInput = () => {
  4513. headerRowInput.value = headerRowIndex.value === null ? '' : String(headerRowIndex.value + 1);
  4514. };
  4515. const applyHeaderRow = (rowIndex: number, options: { silent?: boolean; autoMatch?: boolean } = {}) => {
  4516. const { silent = false, autoMatch = true } = options;
  4517. if (rowIndex < 0 || rowIndex >= sourceTableRows.value.length) {
  4518. syncHeaderRowInput();
  4519. if (!silent) {
  4520. createMessage.warning('请输入正确的表头行号');
  4521. }
  4522. return;
  4523. }
  4524. const previousHeaders = [...rightTableHeaders.value];
  4525. const headerSourceRow = sourceTableRows.value[rowIndex] || ({} as Record<string, string>);
  4526. const nextHeaders = buildHeaderLabelsByRow(previousHeaders, headerSourceRow, addedColumns.value);
  4527. if (
  4528. headerRowIndex.value === rowIndex &&
  4529. nextHeaders.length === previousHeaders.length &&
  4530. nextHeaders.every((header, index) => header === previousHeaders[index])
  4531. ) {
  4532. selectedRowIndex.value = rowIndex;
  4533. syncHeaderRowInput();
  4534. return;
  4535. }
  4536. const headerKeyMap = previousHeaders.reduce(
  4537. (acc, header, index) => {
  4538. acc[header] = nextHeaders[index];
  4539. return acc;
  4540. },
  4541. {} as Record<string, string>,
  4542. );
  4543. const fixedHeaderSet = new Set(addedColumns.value);
  4544. sourceTableRows.value = sourceTableRows.value.map((row, index) => {
  4545. const nextRow: Record<string, string> = {};
  4546. previousHeaders.forEach((header, colIndex) => {
  4547. const nextHeader = nextHeaders[colIndex];
  4548. if (fixedHeaderSet.has(header)) {
  4549. nextRow[nextHeader] = String(row[header] ?? '');
  4550. return;
  4551. }
  4552. nextRow[nextHeader] = index === rowIndex ? nextHeader : String(row[header] ?? '');
  4553. });
  4554. return nextRow;
  4555. });
  4556. rightTableHeaders.value = nextHeaders;
  4557. generatedColumnHeaderRowMap.value = addedColumns.value.reduce(
  4558. (acc, item) => {
  4559. const nextKey = headerKeyMap[item] || item;
  4560. acc[nextKey] = getGeneratedColumnHeaderRowIndex(item);
  4561. return acc;
  4562. },
  4563. {} as Record<string, number | null>,
  4564. );
  4565. addedColumns.value = addedColumns.value.map((item) => headerKeyMap[item] || item);
  4566. selectedAddedColumn.value = selectedAddedColumn.value ? headerKeyMap[selectedAddedColumn.value] || '' : '';
  4567. fieldMapping.value = Object.entries(fieldMapping.value).reduce(
  4568. (acc, [key, value]) => {
  4569. acc[key] = headerKeyMap[value] || value;
  4570. return acc;
  4571. },
  4572. {} as Record<string, string>,
  4573. );
  4574. addedColumnConfig.value = Object.entries(renameRecordKeys(addedColumnConfig.value, headerKeyMap)).reduce(
  4575. (acc, [key, value]) => {
  4576. acc[key] = {
  4577. ...value,
  4578. contentFields: value.contentFields.map((field) => headerKeyMap[field] || field),
  4579. };
  4580. return acc;
  4581. },
  4582. {} as Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>,
  4583. );
  4584. splitColumnConfig.value = Object.entries(renameRecordKeys(splitColumnConfig.value, headerKeyMap)).reduce(
  4585. (acc, [key, value]) => {
  4586. acc[key] = {
  4587. ...value,
  4588. source: headerKeyMap[value.source] || value.source,
  4589. targetName1: headerKeyMap[value.targetName1] || value.targetName1,
  4590. targetName2: headerKeyMap[value.targetName2] || value.targetName2,
  4591. };
  4592. return acc;
  4593. },
  4594. {} as Record<
  4595. string,
  4596. { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
  4597. >,
  4598. );
  4599. selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, headerKeyMap);
  4600. groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, headerKeyMap);
  4601. if (splitColumnForm.value.source) {
  4602. splitColumnForm.value.source = headerKeyMap[splitColumnForm.value.source] || splitColumnForm.value.source;
  4603. }
  4604. headerRowIndex.value = rowIndex;
  4605. selectedRowIndex.value = rowIndex;
  4606. syncHeaderRowInput();
  4607. rebuildGeneratedColumnsInSourceRows();
  4608. runRulePipeline();
  4609. if (autoMatch) {
  4610. autoMatchFields(false);
  4611. }
  4612. if (!silent) {
  4613. createMessage.success(`已将第${rowIndex + 1}行设为表头行`);
  4614. }
  4615. };
  4616. const applyHeaderRowInput = () => {
  4617. const rawValue = headerRowInput.value.trim();
  4618. if (!rawValue) {
  4619. syncHeaderRowInput();
  4620. return;
  4621. }
  4622. const rowNumber = Number(rawValue);
  4623. if (!Number.isInteger(rowNumber)) {
  4624. syncHeaderRowInput();
  4625. createMessage.warning('请输入正确的表头行号');
  4626. return;
  4627. }
  4628. applyHeaderRow(rowNumber - 1);
  4629. };
  4630. const restorePreviewFromNode = async (item?: TreeFileInfo | null) => {
  4631. const previewDataset = await loadPreviewDatasetFromFile(item);
  4632. const hasPresetHeaders = previewDataset ? applyPreviewDataset(previewDataset) : applyPresetHeadersFromNode(item);
  4633. const headerRow = normalizeStoredLineNoToRowIndex(item?.lineNo);
  4634. if (headerRow !== null && !hasPresetHeaders) {
  4635. applyHeaderRow(headerRow, { silent: true, autoMatch: false });
  4636. return;
  4637. }
  4638. headerRowIndex.value = headerRow;
  4639. selectedRowIndex.value = headerRow;
  4640. syncHeaderRowInput();
  4641. };
  4642. const ensureCurrentHeaderRowConfigured = (warningText = '请先填写表头所在行,再进行模板匹配') => {
  4643. if (headerRowIndex.value !== null) return true;
  4644. const rawValue = headerRowInput.value.trim();
  4645. if (!rawValue) {
  4646. createMessage.warning(warningText);
  4647. return false;
  4648. }
  4649. const rowNumber = Number(rawValue);
  4650. if (!Number.isInteger(rowNumber) || rowNumber <= 0 || rowNumber > sourceTableRows.value.length) {
  4651. createMessage.warning('请输入正确的表头行号');
  4652. return false;
  4653. }
  4654. applyHeaderRow(rowNumber - 1, { silent: true, autoMatch: false });
  4655. return headerRowIndex.value !== null;
  4656. };
  4657. const hasNodeTemplateMatched = (item?: TreeFileInfo | null) => {
  4658. if (!isTreeSheetNode(item)) return false;
  4659. return (
  4660. normalizeTemplateId(item?.templateId) > 0 ||
  4661. normalizeTableRuleList(item?.tableRuleList).length > 0 ||
  4662. !!normalizeRuleText(item?.tableName)
  4663. );
  4664. };
  4665. const collectMatchedSheetsWithoutHeaderRow = (items?: TreeFileInfo[] | null) => {
  4666. const missingSheets: string[] = [];
  4667. const walk = (nodes?: TreeFileInfo[] | null) => {
  4668. if (!Array.isArray(nodes) || !nodes.length) return;
  4669. nodes.forEach((node) => {
  4670. if (hasNodeTemplateMatched(node) && normalizeStoredLineNoToRowIndex(node?.lineNo) === null) {
  4671. missingSheets.push(getTreeSheetName(node));
  4672. }
  4673. if (Array.isArray(node.children) && node.children.length) {
  4674. walk(node.children);
  4675. }
  4676. });
  4677. };
  4678. walk(items);
  4679. return missingSheets;
  4680. };
  4681. const mockIndexColumnWidth = 72;
  4682. const mockDataColumnWidth = 168;
  4683. const mockGridTemplate = computed(() =>
  4684. [`${mockIndexColumnWidth}px`, ...rightTableHeaders.value.map(() => `${mockDataColumnWidth}px`)].join(' '),
  4685. );
  4686. const getColumnRuleBadgeState = (colIndex: number) => {
  4687. if (colIndex < 0) return false;
  4688. const rules = sanitizeSelectedRuleList(selectRules.value[colIndex] || []);
  4689. const hasEnabledRules = rules.some((rule) => rule && rule.key && rule.enabled);
  4690. if (hasEnabledRules) return 'active';
  4691. return rules.some((rule) => rule && rule.key) ? 'draft' : '';
  4692. };
  4693. const getColumnRuleBadgeTitle = (colIndex: number) => {
  4694. const state = getColumnRuleBadgeState(colIndex);
  4695. if (state === 'active') return '该列已启用清洗流程';
  4696. if (state === 'draft') return '该列已添加清洗规则,待启用';
  4697. return '';
  4698. };
  4699. const addColumnDialogOpen = ref(false);
  4700. const splitColumnDialogOpen = ref(false);
  4701. const deleteColumnDialogOpen = ref(false);
  4702. const addColumnMode = ref<'add' | 'edit'>('add');
  4703. const addColumnForm = ref<{
  4704. name: string;
  4705. mode: 'ref' | 'custom';
  4706. contentFields: string[];
  4707. customContent: string;
  4708. }>({
  4709. name: '',
  4710. mode: 'ref',
  4711. contentFields: [],
  4712. customContent: '',
  4713. });
  4714. const addedColumnConfig = ref<
  4715. Record<string, { mode: 'ref' | 'custom'; contentFields: string[]; customContent: string }>
  4716. >({});
  4717. const splitColumnConfig = ref<
  4718. Record<
  4719. string,
  4720. { source: string; delimiterType: string; delimiterValue: string; targetName1: string; targetName2: string }
  4721. >
  4722. >({});
  4723. const selectedSourceField = ref('');
  4724. const selectedContentIndex = ref(-1);
  4725. const draggingContentIndex = ref<number | null>(null);
  4726. const dragOverContentIndex = ref<number | null>(null);
  4727. const availableSourceHeaders = computed(() =>
  4728. rightTableHeaders.value.filter((item) => item && !addedColumns.value.includes(item)),
  4729. );
  4730. const sourceFieldCandidates = computed(() => availableSourceHeaders.value);
  4731. const normalizeColumnNameForCompare = (value: unknown) =>
  4732. String(value ?? '')
  4733. .trim()
  4734. .toLocaleLowerCase();
  4735. const hasDuplicateHeaderName = (name: string, excludedRawNames: string[] = []) => {
  4736. const normalizedName = normalizeColumnNameForCompare(name);
  4737. if (!normalizedName) return false;
  4738. return rightTableHeaders.value.some(
  4739. (header) => !excludedRawNames.includes(header) && normalizeColumnNameForCompare(header) === normalizedName,
  4740. );
  4741. };
  4742. const normalizeSplitColumnMode = (value?: unknown) => {
  4743. const rawMode = normalizeRuleText(value);
  4744. if (rawMode === 'length' || rawMode === 'special') {
  4745. return rawMode as 'length' | 'special';
  4746. }
  4747. if (rawMode === 'comma' || rawMode === 'space' || rawMode === 'custom') {
  4748. return 'special' as const;
  4749. }
  4750. return 'length' as const;
  4751. };
  4752. const normalizeSplitColumnValue = (type?: unknown, value?: unknown) => {
  4753. const rawType = normalizeRuleText(type);
  4754. if (rawType === 'comma') return ',';
  4755. if (rawType === 'space') return ' ';
  4756. return normalizeRuleText(value);
  4757. };
  4758. const resolveSplitColumnLength = (type?: unknown, value?: unknown) => {
  4759. if (normalizeSplitColumnMode(type) !== 'length') return 0;
  4760. const length = Number(normalizeSplitColumnValue(type, value));
  4761. return Number.isInteger(length) && length > 0 ? length : 0;
  4762. };
  4763. const splitSourceValueByConfig = (sourceValue: string, type?: unknown, value?: unknown) => {
  4764. const splitMode = normalizeSplitColumnMode(type);
  4765. const normalizedValue = normalizeSplitColumnValue(type, value);
  4766. if (!sourceValue) {
  4767. return { first: '', second: '' };
  4768. }
  4769. if (splitMode === 'length') {
  4770. const splitLength = resolveSplitColumnLength(type, value);
  4771. if (!splitLength) {
  4772. return { first: sourceValue, second: '' };
  4773. }
  4774. return {
  4775. first: sourceValue.slice(0, splitLength),
  4776. second: sourceValue.slice(splitLength),
  4777. };
  4778. }
  4779. if (!normalizedValue) {
  4780. return { first: sourceValue, second: '' };
  4781. }
  4782. const splitIndex = sourceValue.indexOf(normalizedValue);
  4783. if (splitIndex < 0) {
  4784. return { first: sourceValue, second: '' };
  4785. }
  4786. return {
  4787. first: sourceValue.slice(0, splitIndex),
  4788. second: sourceValue.slice(splitIndex + normalizedValue.length),
  4789. };
  4790. };
  4791. const splitColumnForm = ref({
  4792. source: '',
  4793. targetName1: '',
  4794. targetName2: '',
  4795. delimiterType: 'length',
  4796. delimiterValue: '',
  4797. });
  4798. const delimiterOptions = [
  4799. { label: '按字符长度', value: 'length' },
  4800. { label: '按特殊标识', value: 'special' },
  4801. ];
  4802. const columnTypeOptions = [
  4803. { label: '字符串', value: 'string' },
  4804. { label: '数字', value: 'number' },
  4805. { label: '日期时间', value: 'datetime' },
  4806. ];
  4807. const splitSourceOptions = computed(() =>
  4808. availableSourceHeaders.value.map((item) => ({
  4809. label: item,
  4810. value: item,
  4811. })),
  4812. );
  4813. const originalFieldOptions = computed(() =>
  4814. rightTableHeaders.value.map((item) => ({
  4815. label: item,
  4816. value: item,
  4817. })),
  4818. );
  4819. const fieldMapping = ref<Record<string, string>>({});
  4820. const mappingPopoverOpenKey = ref<string>('');
  4821. const originFilterKeyword = ref('');
  4822. const hoveredOriginOptionTargetKey = ref('');
  4823. const hoveredOriginOptionValue = ref('');
  4824. const originHoverPreviewSnapshot = ref<PreviewColumnSnapshot | null>(null);
  4825. const recognitionAmountZfEnabled = ref(false);
  4826. const resolveTargetKeyByTableRule = (rule: TreeTableRuleInfo) => {
  4827. const fieldNameEn = normalizeRuleText(rule.fieldNameEn);
  4828. if (fieldNameEn) {
  4829. const matchedByKey = fixedTargetFields.value.find((item) => item.key === fieldNameEn);
  4830. if (matchedByKey) return matchedByKey.key;
  4831. }
  4832. const fieldNameCn = normalizeRuleText(rule.fieldNameCn);
  4833. if (fieldNameCn) {
  4834. const matchedByLabel =
  4835. fixedTargetFields.value.find((item) => item.label === fieldNameCn) ||
  4836. fixedTargetFields.value.find((item) => item.label.includes(fieldNameCn) || fieldNameCn.includes(item.label));
  4837. if (matchedByLabel) return matchedByLabel.key;
  4838. }
  4839. return '';
  4840. };
  4841. const applyTableRuleMapping = (list?: TreeTableRuleInfo[] | null) => {
  4842. const tableRules = normalizeTableRuleList(list);
  4843. if (!tableRules.length || !fixedTargetFields.value.length) return false;
  4844. const headerSet = new Set(rightTableHeaders.value);
  4845. const nextMapping = tableRules.reduce(
  4846. (acc, item) => {
  4847. const targetKey = resolveTargetKeyByTableRule(item);
  4848. const sourceField = normalizeRuleText(item.fileColName);
  4849. if (!targetKey || !sourceField || !headerSet.has(sourceField)) return acc;
  4850. acc[targetKey] = sourceField;
  4851. return acc;
  4852. },
  4853. {} as Record<string, string>,
  4854. );
  4855. if (!Object.keys(nextMapping).length) return false;
  4856. fieldMapping.value = { ...fieldMapping.value, ...nextMapping };
  4857. return true;
  4858. };
  4859. const filteredOriginalOptions = computed(() => {
  4860. if (!originFilterKeyword.value) return originalFieldOptions.value;
  4861. return originalFieldOptions.value.filter((item) => item.label.includes(originFilterKeyword.value));
  4862. });
  4863. const resolveRecognitionBasicInfoTargetKey = (
  4864. targetField: unknown,
  4865. fields: MappingTargetField[] = fixedTargetFields.value,
  4866. ) => {
  4867. const rawTargetField = normalizeRecognitionConfigText(targetField);
  4868. if (!rawTargetField || !fields.length) return '';
  4869. const matchedByKey = fields.find((item) => item.key === rawTargetField);
  4870. if (matchedByKey) return matchedByKey.key;
  4871. const matchedByLabel = fields.find((item) => item.label === rawTargetField);
  4872. if (matchedByLabel) return matchedByLabel.key;
  4873. const normalizedTargetField = normalizeFieldMatchText(rawTargetField);
  4874. if (!normalizedTargetField) return '';
  4875. const matchedByNormalized = fields.find(
  4876. (item) =>
  4877. normalizeFieldMatchText(item.key) === normalizedTargetField ||
  4878. normalizeFieldMatchText(item.label) === normalizedTargetField,
  4879. );
  4880. if (matchedByNormalized) return matchedByNormalized.key;
  4881. const matchedByFuzzy = fields.find((item) => {
  4882. const normalizedKey = normalizeFieldMatchText(item.key);
  4883. const normalizedLabel = normalizeFieldMatchText(item.label);
  4884. return (
  4885. (!!normalizedKey &&
  4886. (normalizedKey.includes(normalizedTargetField) || normalizedTargetField.includes(normalizedKey))) ||
  4887. (!!normalizedLabel &&
  4888. (normalizedLabel.includes(normalizedTargetField) || normalizedTargetField.includes(normalizedLabel)))
  4889. );
  4890. });
  4891. return matchedByFuzzy?.key || '';
  4892. };
  4893. const buildRecognitionBasicInfoMatchedTargetKeySet = (
  4894. rows: RecognitionBasicInfoRow[] = [],
  4895. fields: MappingTargetField[] = fixedTargetFields.value,
  4896. ) =>
  4897. rows.reduce((acc, item) => {
  4898. if (!normalizeRecognitionConfigText(item.headerMatch)) return acc;
  4899. const targetKey = resolveRecognitionBasicInfoTargetKey(item.targetField, fields);
  4900. if (targetKey) {
  4901. acc.add(targetKey);
  4902. }
  4903. return acc;
  4904. }, new Set<string>());
  4905. const buildEffectiveFieldMapping = (mapping: Record<string, string>, lockedTargetKeySet: Set<string>) =>
  4906. Object.entries(mapping).reduce(
  4907. (acc, [targetKey, header]) => {
  4908. const mappedHeader = normalizeRuleText(header);
  4909. if (!mappedHeader || lockedTargetKeySet.has(targetKey)) return acc;
  4910. acc[targetKey] = mappedHeader;
  4911. return acc;
  4912. },
  4913. {} as Record<string, string>,
  4914. );
  4915. const recognitionBasicInfoMatchedTargetKeySet = computed(() =>
  4916. buildRecognitionBasicInfoMatchedTargetKeySet(recognitionBasicInfoRows.value, fixedTargetFields.value),
  4917. );
  4918. const isTargetMatchedByRecognitionBasicInfo = (targetKey: string) =>
  4919. recognitionBasicInfoMatchedTargetKeySet.value.has(targetKey);
  4920. const getTargetFieldByKey = (targetKey: string, fields: MappingTargetField[] = fixedTargetFields.value) =>
  4921. fields.find((item) => item.key === targetKey) || null;
  4922. const buildAmountZfHandledTargetKeySet = (
  4923. fields: MappingTargetField[] = fixedTargetFields.value,
  4924. enabled = recognitionAmountZfEnabled.value,
  4925. ) =>
  4926. fields.reduce((acc, field) => {
  4927. if (enabled && isLoanFlagTargetField(field)) {
  4928. acc.add(field.key);
  4929. }
  4930. return acc;
  4931. }, new Set<string>());
  4932. const isTargetHandledByAmountZf = (targetKey: string) => {
  4933. const field = getTargetFieldByKey(targetKey);
  4934. return !!field && recognitionAmountZfEnabled.value && isLoanFlagTargetField(field);
  4935. };
  4936. const recognitionRuleHandledTargetKeySet = computed(() => {
  4937. const nextSet = new Set(recognitionBasicInfoMatchedTargetKeySet.value);
  4938. buildAmountZfHandledTargetKeySet(fixedTargetFields.value, recognitionAmountZfEnabled.value).forEach((key) => {
  4939. nextSet.add(key);
  4940. });
  4941. return nextSet;
  4942. });
  4943. const isTargetHandledByRecognitionRule = (targetKey: string) =>
  4944. recognitionRuleHandledTargetKeySet.value.has(targetKey);
  4945. const effectiveFieldMapping = computed(() =>
  4946. buildEffectiveFieldMapping(fieldMapping.value, recognitionRuleHandledTargetKeySet.value),
  4947. );
  4948. const getDisplayedMappedHeader = (targetKey: string) =>
  4949. isTargetHandledByRecognitionRule(targetKey) ? '' : fieldMapping.value[targetKey] || '';
  4950. const isTargetMatched = (targetKey: string) =>
  4951. isTargetHandledByRecognitionRule(targetKey) || !!effectiveFieldMapping.value[targetKey];
  4952. const resolveMappedTargetKeyByHeader = (header: string) => {
  4953. const normalizedHeader = normalizeRuleText(header);
  4954. if (!normalizedHeader) return '';
  4955. return (
  4956. fixedTargetFields.value.find((item) => normalizeRuleText(getDisplayedMappedHeader(item.key)) === normalizedHeader)
  4957. ?.key || ''
  4958. );
  4959. };
  4960. const getPreviewColumnTarget = (colIndex: number) => {
  4961. const container = tableMockRef.value;
  4962. if (!container) return null;
  4963. return (
  4964. container.querySelector<HTMLElement>(`.mock-row > span[data-preview-col="${colIndex}"]`) ||
  4965. container.querySelector<HTMLElement>(`.mock-match-row > span[data-preview-col="${colIndex}"]`) ||
  4966. container.querySelector<HTMLElement>(`.mock-head > span[data-preview-col="${colIndex}"]`) ||
  4967. null
  4968. );
  4969. };
  4970. const isVisibleInDom = (element: HTMLElement) => {
  4971. return element.offsetParent !== null || element.getClientRects().length > 0;
  4972. };
  4973. const getVisibleOriginOptionList = (targetKey: string) => {
  4974. const lists = Array.from(
  4975. document.querySelectorAll<HTMLElement>(
  4976. `.origin-field-popover .origin-pop-list[data-origin-list-target-key="${targetKey}"]`,
  4977. ),
  4978. );
  4979. return lists.find((item) => isVisibleInDom(item)) || lists[0] || null;
  4980. };
  4981. const scrollOriginOptionIntoView = (targetKey: string) => {
  4982. const optionValue = normalizeRuleText(getDisplayedMappedHeader(targetKey));
  4983. if (!optionValue) return;
  4984. void nextTick(() => {
  4985. const tryScroll = (attempt = 0) => {
  4986. const list = getVisibleOriginOptionList(targetKey);
  4987. if (!list) {
  4988. if (attempt < 6) {
  4989. window.requestAnimationFrame(() => tryScroll(attempt + 1));
  4990. }
  4991. return;
  4992. }
  4993. const options = Array.from(
  4994. list.querySelectorAll<HTMLElement>(`[data-origin-target-key="${targetKey}"][data-origin-option-value]`),
  4995. );
  4996. const option =
  4997. options.find((item) => normalizeRuleText(item.dataset.originOptionValue) === optionValue) || null;
  4998. const scrollTarget = option?.closest<HTMLElement>('.origin-tag-wrap') || option;
  4999. scrollTarget?.scrollIntoView({
  5000. block: 'nearest',
  5001. inline: 'nearest',
  5002. behavior: 'smooth',
  5003. });
  5004. };
  5005. window.requestAnimationFrame(() => {
  5006. window.requestAnimationFrame(() => {
  5007. tryScroll();
  5008. });
  5009. });
  5010. });
  5011. };
  5012. const scrollPreviewColumnIntoView = (colIndex: number) => {
  5013. if (colIndex < 0) return;
  5014. void nextTick(() => {
  5015. window.requestAnimationFrame(() => {
  5016. window.requestAnimationFrame(() => {
  5017. const container = tableMockRef.value;
  5018. const target = getPreviewColumnTarget(colIndex);
  5019. if (!container || !target) return;
  5020. const containerRect = container.getBoundingClientRect();
  5021. const targetRect = target.getBoundingClientRect();
  5022. const targetLeft = container.scrollLeft + (targetRect.left - containerRect.left);
  5023. const desiredLeft = targetLeft - mockIndexColumnWidth - 16;
  5024. const maxScrollLeft = Math.max(container.scrollWidth - container.clientWidth, 0);
  5025. container.scrollTo({
  5026. left: Math.min(Math.max(desiredLeft, 0), maxScrollLeft),
  5027. behavior: 'smooth',
  5028. });
  5029. });
  5030. });
  5031. });
  5032. };
  5033. const applyPreviewColumnState = (
  5034. header: string,
  5035. colIndex: number,
  5036. options: {
  5037. resetSelectedRow?: boolean;
  5038. activeTargetKey?: string;
  5039. } = {},
  5040. ) => {
  5041. if (colIndex < 0) return;
  5042. currentColumn.value = colIndex;
  5043. if (options.resetSelectedRow ?? true) {
  5044. selectedRowIndex.value = null;
  5045. }
  5046. selectedAddedColumn.value = addedColumns.value.includes(header) ? header : '';
  5047. activeMappingTargetKey.value =
  5048. options.activeTargetKey !== undefined ? options.activeTargetKey : resolveMappedTargetKeyByHeader(header);
  5049. scrollPreviewColumnIntoView(colIndex);
  5050. };
  5051. const capturePreviewColumnSnapshot = (): PreviewColumnSnapshot => ({
  5052. colIndex: currentColumn.value,
  5053. selectedRowIndex: selectedRowIndex.value,
  5054. selectedAddedColumn: selectedAddedColumn.value,
  5055. activeTargetKey: activeMappingTargetKey.value,
  5056. });
  5057. const clearOriginHoverPreviewState = () => {
  5058. hoveredOriginOptionTargetKey.value = '';
  5059. hoveredOriginOptionValue.value = '';
  5060. originHoverPreviewSnapshot.value = null;
  5061. };
  5062. const restoreOriginHoverPreview = (targetKey?: string) => {
  5063. if (targetKey && hoveredOriginOptionTargetKey.value && hoveredOriginOptionTargetKey.value !== targetKey) {
  5064. return;
  5065. }
  5066. const snapshot = originHoverPreviewSnapshot.value;
  5067. clearOriginHoverPreviewState();
  5068. if (!snapshot) return;
  5069. currentColumn.value = snapshot.colIndex;
  5070. selectedRowIndex.value = snapshot.selectedRowIndex;
  5071. selectedAddedColumn.value = snapshot.selectedAddedColumn;
  5072. activeMappingTargetKey.value = snapshot.activeTargetKey;
  5073. if (snapshot.colIndex !== null && snapshot.colIndex >= 0) {
  5074. scrollPreviewColumnIntoView(snapshot.colIndex);
  5075. }
  5076. };
  5077. const focusPreviewColumn = (
  5078. header: string,
  5079. colIndex: number,
  5080. options: {
  5081. resetSelectedRow?: boolean;
  5082. activeTargetKey?: string;
  5083. } = {},
  5084. ) => {
  5085. applyPreviewColumnState(header, colIndex, options);
  5086. };
  5087. const syncMappingSelectionState = (targetKey: string) => {
  5088. activeMappingTargetKey.value = targetKey;
  5089. const mappedHeader = normalizeRuleText(getDisplayedMappedHeader(targetKey));
  5090. if (mappedHeader) {
  5091. const colIndex = rightTableHeaders.value.findIndex((header) => normalizeRuleText(header) === mappedHeader);
  5092. if (colIndex >= 0) {
  5093. focusPreviewColumn(rightTableHeaders.value[colIndex], colIndex, {
  5094. resetSelectedRow: true,
  5095. activeTargetKey: targetKey,
  5096. });
  5097. return;
  5098. }
  5099. }
  5100. const targetLabel = normalizeFieldMatchText(
  5101. fixedTargetFields.value.find((item) => item.key === targetKey)?.label || '',
  5102. );
  5103. if (!targetLabel) return;
  5104. const fallbackColIndex = rightTableHeaders.value.findIndex(
  5105. (header) => normalizeFieldMatchText(header) === targetLabel,
  5106. );
  5107. if (fallbackColIndex < 0) return;
  5108. focusPreviewColumn(rightTableHeaders.value[fallbackColIndex], fallbackColIndex, {
  5109. resetSelectedRow: true,
  5110. activeTargetKey: targetKey,
  5111. });
  5112. };
  5113. const handleMappingRowSelect = (targetKey: string) => {
  5114. syncMappingSelectionState(targetKey);
  5115. if (!isTargetHandledByRecognitionRule(targetKey)) {
  5116. mappingPopoverOpenKey.value = targetKey;
  5117. originFilterKeyword.value = '';
  5118. scrollOriginOptionIntoView(targetKey);
  5119. }
  5120. };
  5121. const handleOriginOptionHover = (targetKey: string, fieldValue: string) => {
  5122. const normalizedFieldValue = normalizeRuleText(fieldValue);
  5123. if (!normalizedFieldValue) return;
  5124. if (!originHoverPreviewSnapshot.value || hoveredOriginOptionTargetKey.value !== targetKey) {
  5125. originHoverPreviewSnapshot.value = capturePreviewColumnSnapshot();
  5126. }
  5127. hoveredOriginOptionTargetKey.value = targetKey;
  5128. hoveredOriginOptionValue.value = fieldValue;
  5129. const colIndex = rightTableHeaders.value.findIndex((header) => normalizeRuleText(header) === normalizedFieldValue);
  5130. if (colIndex < 0) return;
  5131. applyPreviewColumnState(rightTableHeaders.value[colIndex], colIndex, {
  5132. resetSelectedRow: true,
  5133. activeTargetKey: targetKey,
  5134. });
  5135. };
  5136. const handleOriginOptionHoverLeave = (targetKey: string) => {
  5137. restoreOriginHoverPreview(targetKey);
  5138. };
  5139. const isOriginOptionHovering = (targetKey: string, fieldValue: string) => {
  5140. return (
  5141. hoveredOriginOptionTargetKey.value === targetKey &&
  5142. normalizeRuleText(hoveredOriginOptionValue.value) === normalizeRuleText(fieldValue)
  5143. );
  5144. };
  5145. const isFieldTakenByOther = (fieldValue: string, currentKey: string) => {
  5146. return Object.entries(effectiveFieldMapping.value).some(
  5147. ([key, value]) => key !== currentKey && value === fieldValue,
  5148. );
  5149. };
  5150. const handleMappingPopoverOpen = (key: string, open: boolean) => {
  5151. if (isTargetHandledByRecognitionRule(key)) {
  5152. mappingPopoverOpenKey.value = '';
  5153. originFilterKeyword.value = '';
  5154. restoreOriginHoverPreview(key);
  5155. return;
  5156. }
  5157. mappingPopoverOpenKey.value = open ? key : '';
  5158. if (open) {
  5159. handleMappingRowSelect(key);
  5160. }
  5161. if (!open) {
  5162. restoreOriginHoverPreview(key);
  5163. originFilterKeyword.value = '';
  5164. }
  5165. };
  5166. const handleMappingTriggerClick = (targetKey: string) => {
  5167. if (isTargetHandledByRecognitionRule(targetKey)) {
  5168. activeMappingTargetKey.value = targetKey;
  5169. mappingPopoverOpenKey.value = '';
  5170. originFilterKeyword.value = '';
  5171. return;
  5172. }
  5173. handleMappingRowSelect(targetKey);
  5174. mappingPopoverOpenKey.value = targetKey;
  5175. };
  5176. const closeMappingPopoverPreview = () => {
  5177. mappingPopoverOpenKey.value = '';
  5178. originFilterKeyword.value = '';
  5179. clearOriginHoverPreviewState();
  5180. };
  5181. const setFieldBlank = (targetKey: string) => {
  5182. if (isTargetHandledByRecognitionRule(targetKey)) return;
  5183. restoreOriginHoverPreview(targetKey);
  5184. activeMappingTargetKey.value = targetKey;
  5185. fieldMapping.value[targetKey] = '';
  5186. refreshRecognitionDirectionState(activeNodeInfo.value);
  5187. mappingPopoverOpenKey.value = '';
  5188. };
  5189. const selectOriginalField = (targetKey: string, fieldValue: string) => {
  5190. if (isTargetHandledByRecognitionRule(targetKey)) return;
  5191. if (isFieldTakenByOther(fieldValue, targetKey)) return;
  5192. if (normalizeRuleText(getDisplayedMappedHeader(targetKey)) === normalizeRuleText(fieldValue)) {
  5193. setFieldBlank(targetKey);
  5194. return;
  5195. }
  5196. clearOriginHoverPreviewState();
  5197. fieldMapping.value[targetKey] = fieldValue;
  5198. refreshRecognitionDirectionState(activeNodeInfo.value);
  5199. mappingPopoverOpenKey.value = '';
  5200. syncMappingSelectionState(targetKey);
  5201. };
  5202. const replaceOriginalField = (targetKey: string, fieldValue: string) => {
  5203. if (isTargetHandledByRecognitionRule(targetKey)) return;
  5204. clearOriginHoverPreviewState();
  5205. Object.keys(effectiveFieldMapping.value).forEach((key) => {
  5206. if (key !== targetKey && effectiveFieldMapping.value[key] === fieldValue) {
  5207. fieldMapping.value[key] = '';
  5208. }
  5209. });
  5210. fieldMapping.value[targetKey] = fieldValue;
  5211. refreshRecognitionDirectionState(activeNodeInfo.value);
  5212. mappingPopoverOpenKey.value = '';
  5213. syncMappingSelectionState(targetKey);
  5214. };
  5215. const childTemplateName = ref('');
  5216. const showChildTemplateExistsWarning = (name: string) => {
  5217. Modal.warning({
  5218. title: '子模板名称已存在',
  5219. content: `子模板名称“${name}”已存在,请修改后再导入数据。`,
  5220. centered: true,
  5221. });
  5222. };
  5223. const showDuplicateChildTemplateWarning = (names: string[]) => {
  5224. const previewNames = names.slice(0, 3).join('、');
  5225. const suffix = names.length > 3 ? ` 等 ${names.length} 个名称` : '';
  5226. Modal.warning({
  5227. title: '子模板名称重复',
  5228. content: `当前清洗结果中存在重复的子模板名称:${previewNames}${suffix},请修改后再导入数据。`,
  5229. centered: true,
  5230. });
  5231. };
  5232. const checkTemplateNameAvailable = async (tableNameCn: string) => {
  5233. const response = await tplCheckTableName('', tableNameCn);
  5234. // const available = resolveTemplateNameCheckResult(response);
  5235. // if (available === null) {
  5236. // throw new Error('模板名称校验结果无法识别');
  5237. // }
  5238. return response === 'N';
  5239. };
  5240. const handleChildTemplateNameBlur = async () => {
  5241. const tableNameCn = normalizeRuleText(childTemplateName.value);
  5242. if (!tableNameCn) return;
  5243. try {
  5244. const available = await checkTemplateNameAvailable(tableNameCn);
  5245. if (!available) {
  5246. showChildTemplateExistsWarning(tableNameCn);
  5247. }
  5248. } catch (error) {
  5249. createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
  5250. }
  5251. };
  5252. const collectChildTemplateNames = (nodes: TreeFileInfo[] = []) => {
  5253. const names: string[] = [];
  5254. const walk = (items: TreeFileInfo[] = []) => {
  5255. items.forEach((item) => {
  5256. const childName = normalizeRuleText(item.childTemplateName);
  5257. if (childName) {
  5258. names.push(childName);
  5259. }
  5260. if (Array.isArray(item.children) && item.children.length) {
  5261. walk(item.children);
  5262. }
  5263. });
  5264. };
  5265. walk(nodes);
  5266. return names;
  5267. };
  5268. const findDuplicateChildTemplateNames = (names: string[]) => {
  5269. const duplicateNameMap = new Map<string, string>();
  5270. const counter = new Map<string, number>();
  5271. names.forEach((name) => {
  5272. const key = normalizeTemplateNameKey(name);
  5273. if (!key) return;
  5274. const nextCount = (counter.get(key) || 0) + 1;
  5275. counter.set(key, nextCount);
  5276. if (nextCount > 1 && !duplicateNameMap.has(key)) {
  5277. duplicateNameMap.set(key, name);
  5278. }
  5279. });
  5280. return Array.from(duplicateNameMap.values());
  5281. };
  5282. const ensureChildTemplateNamesAvailable = async (nodes: TreeFileInfo[] = []) => {
  5283. const childTemplateNames = collectChildTemplateNames(nodes);
  5284. if (!childTemplateNames.length) return true;
  5285. const duplicateNames = findDuplicateChildTemplateNames(childTemplateNames);
  5286. if (duplicateNames.length) {
  5287. showDuplicateChildTemplateWarning(duplicateNames);
  5288. return false;
  5289. }
  5290. const uniqueNameMap = new Map<string, string>();
  5291. childTemplateNames.forEach((name) => {
  5292. const key = normalizeTemplateNameKey(name);
  5293. if (key && !uniqueNameMap.has(key)) {
  5294. uniqueNameMap.set(key, name);
  5295. }
  5296. });
  5297. for (const name of uniqueNameMap.values()) {
  5298. const available = await checkTemplateNameAvailable(name);
  5299. if (!available) {
  5300. showChildTemplateExistsWarning(name);
  5301. return false;
  5302. }
  5303. }
  5304. return true;
  5305. };
  5306. const cleaningRulePanelExpanded = ref(false);
  5307. const recognitionPanelExpanded = ref(false);
  5308. const activeRecognitionRule = ref('');
  5309. const recognitionDirectionFields = ref<RecognitionDirectionField[]>([]);
  5310. const recognitionBasicInfoBaseFuncRegx = ref('');
  5311. const recognitionBasicInfoManualVisible = ref(false);
  5312. const recognitionBasicInfoExpanded = ref(false);
  5313. const recognitionBasicInfoRows = ref<RecognitionBasicInfoRow[]>([]);
  5314. const recognitionBasicInfoRegex = /^\{(.*?)\}$/s;
  5315. const fixedRecognitionBasicInfoFieldBindings = {
  5316. personCertNo: '客户证件号码',
  5317. personName: '客户名称',
  5318. personCardNo: '本方卡号',
  5319. } as const;
  5320. const normalizedFixedRecognitionBasicInfoFieldMap = Object.entries(fixedRecognitionBasicInfoFieldBindings).reduce(
  5321. (acc, [fieldKey, targetField]) => {
  5322. const normalizedFieldKey = normalizeFieldMatchText(fieldKey);
  5323. const normalizedTargetField = normalizeFieldMatchText(targetField);
  5324. if (normalizedFieldKey) {
  5325. acc[normalizedFieldKey] = targetField;
  5326. }
  5327. if (normalizedTargetField) {
  5328. acc[normalizedTargetField] = targetField;
  5329. }
  5330. return acc;
  5331. },
  5332. {} as Record<string, string>,
  5333. );
  5334. const requiredTotal = computed(() => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item)).length);
  5335. const requiredMatchedCount = computed(
  5336. () => fixedTargetFields.value.filter((item) => isMappingTargetRequired(item) && isTargetMatched(item.key)).length,
  5337. );
  5338. const matchedFieldCount = computed(() => fixedTargetFields.value.filter((item) => isTargetMatched(item.key)).length);
  5339. const showRecognitionBasicInfoPanel = computed(
  5340. () => !!recognitionBasicInfoBaseFuncRegx.value || recognitionBasicInfoRows.value.length > 0,
  5341. );
  5342. const parseRecognitionDirectionEntries = (value: unknown): RecognitionDirectionItem[] => {
  5343. const rawText = normalizeRecognitionConfigText(value);
  5344. if (!rawText) return [];
  5345. try {
  5346. let parsedValue: Record<string, any> | null = null;
  5347. try {
  5348. parsedValue = JSON.parse(rawText);
  5349. } catch (jsonError) {
  5350. void jsonError;
  5351. const normalizedText = rawText
  5352. .replace(/([{,]\s*)'([^']+?)'\s*:/g, '$1"$2":')
  5353. .replace(/:\s*'([^']*?)'(\s*[,}])/g, ': "$1"$2');
  5354. parsedValue = JSON.parse(normalizedText);
  5355. }
  5356. if (!parsedValue || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) return [];
  5357. return Object.entries(parsedValue).reduce((acc, [label, itemValue], index) => {
  5358. const resolvedLabel = String(label || '').trim();
  5359. if (!resolvedLabel) return acc;
  5360. acc.push({
  5361. key: `${resolvedLabel}_${index}`,
  5362. label: resolvedLabel,
  5363. value: itemValue === null || itemValue === undefined ? '' : String(itemValue),
  5364. });
  5365. return acc;
  5366. }, [] as RecognitionDirectionItem[]);
  5367. } catch (error) {
  5368. console.error('解析 directionConf 失败', error);
  5369. return [];
  5370. }
  5371. };
  5372. const buildRecognitionDirectionConf = (entries?: RecognitionDirectionItem[]) => {
  5373. if (!entries?.length) return '';
  5374. const payload = entries.reduce(
  5375. (acc, item) => {
  5376. const label = normalizeRecognitionConfigText(item.label);
  5377. if (!label) return acc;
  5378. acc[label] = normalizeRecognitionConfigText(item.value);
  5379. return acc;
  5380. },
  5381. {} as Record<string, string>,
  5382. );
  5383. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  5384. };
  5385. const resolveRecognitionDirectionTab = (
  5386. fieldLabel: string,
  5387. entries: RecognitionDirectionItem[] = [],
  5388. ): RecognitionRuleTabKey | '' => {
  5389. const mergedText = `${fieldLabel}|${entries.map((item) => item.label).join('|')}`;
  5390. if (fieldLabel.includes('借贷') || /(收入|支出|借方|贷方)/.test(mergedText)) {
  5391. return '借贷标志';
  5392. }
  5393. if (/(通话|短信|通信|话单)/.test(fieldLabel) || /(主叫|被叫|发送|接收|呼入|呼出|上行|下行)/.test(mergedText)) {
  5394. return '通信类别';
  5395. }
  5396. return '';
  5397. };
  5398. const resolveRecognitionDirectionFields = (
  5399. node?: TreeFileInfo | null,
  5400. options: { preferTemplate?: boolean; currentFields?: RecognitionDirectionField[] } = {},
  5401. ) => {
  5402. const currentFieldMap = (options.currentFields || []).reduce(
  5403. (acc, item) => {
  5404. acc[item.key] = item;
  5405. return acc;
  5406. },
  5407. {} as Record<string, RecognitionDirectionField>,
  5408. );
  5409. const tableRuleMap = normalizeTableRuleList(node?.tableRuleList).reduce(
  5410. (acc, item, index) => {
  5411. acc[getTableRuleTargetKey(item, index)] = item;
  5412. return acc;
  5413. },
  5414. {} as Record<string, TreeTableRuleInfo>,
  5415. );
  5416. return fixedTargetFields.value.reduce((acc, field, index) => {
  5417. const templateFieldRecord = (resolveTemplateFieldMeta(field, index) || {}) as Record<string, any>;
  5418. const ruleFieldRecord = tableRuleMap[field.key] || null;
  5419. const currentFieldRecord = currentFieldMap[field.key];
  5420. const templateDirectionConf = normalizeRecognitionConfigText(
  5421. templateFieldRecord.directionConf ?? templateFieldRecord.direction_conf,
  5422. );
  5423. const ruleDirectionConf = normalizeRecognitionConfigText(ruleFieldRecord?.directionConf);
  5424. const currentDirectionConf = currentFieldRecord?.entries?.length
  5425. ? buildRecognitionDirectionConf(currentFieldRecord.entries)
  5426. : '';
  5427. const directionConfText = currentDirectionConf
  5428. ? currentDirectionConf
  5429. : options.preferTemplate
  5430. ? templateDirectionConf
  5431. : ruleDirectionConf || templateDirectionConf;
  5432. const baseEntries = currentFieldRecord?.entries?.length
  5433. ? currentFieldRecord.entries.map((entry) => ({ ...entry }))
  5434. : parseRecognitionDirectionEntries(directionConfText);
  5435. const baseTab = currentFieldRecord?.tab || resolveRecognitionDirectionTab(field.label, baseEntries);
  5436. const entries = baseTab
  5437. ? buildRecognitionDirectionEntriesByMappedColumn(field, baseTab, baseEntries)
  5438. : baseEntries;
  5439. const tab = currentFieldRecord?.tab || resolveRecognitionDirectionTab(field.label, entries);
  5440. if (!entries.length || !tab) return acc;
  5441. acc.push({
  5442. key: field.key,
  5443. label: field.label,
  5444. tab,
  5445. entries,
  5446. });
  5447. return acc;
  5448. }, [] as RecognitionDirectionField[]);
  5449. };
  5450. const applyRecognitionDirectionState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  5451. recognitionDirectionFields.value = resolveRecognitionDirectionFields(node, options);
  5452. };
  5453. const activeRecognitionDirectionFields = computed(() =>
  5454. recognitionDirectionFields.value.filter((item) => item.tab === activeRecognitionRule.value),
  5455. );
  5456. const hasRecognitionLoanTab = computed(() =>
  5457. recognitionDirectionFields.value.some((item) => item.entries.length && item.tab === '借贷标志'),
  5458. );
  5459. const hasRecognitionCommunicationTab = computed(() =>
  5460. recognitionDirectionFields.value.some((item) => item.entries.length && item.tab === '通信类别'),
  5461. );
  5462. const splitRecognitionDirectionKeywords = (value?: string) =>
  5463. normalizeRecognitionConfigText(value)
  5464. .split(/[\n,,;;、]+/)
  5465. .map((item) => item.trim())
  5466. .filter(Boolean);
  5467. const normalizeRecognitionDirectionGroup = (value: string, tab: RecognitionRuleTabKey) => {
  5468. if (tab === '借贷标志') {
  5469. return normalizeLoanFlagCoreValue(value);
  5470. }
  5471. const normalized = normalizeCallTypeCoreValue(value);
  5472. if (['主叫', '发送', '呼出', '上行'].includes(normalized)) {
  5473. return '主叫';
  5474. }
  5475. if (['被叫', '接收', '呼入', '下行'].includes(normalized)) {
  5476. return '被叫';
  5477. }
  5478. return '';
  5479. };
  5480. const mergeRecognitionDirectionKeywords = (currentValue = '', nextValues: string[] = []) => {
  5481. const mergedValues: string[] = [];
  5482. const normalizedValueSet = new Set<string>();
  5483. [...splitRecognitionDirectionKeywords(currentValue), ...nextValues].forEach((item) => {
  5484. const text = normalizeRecognitionConfigText(item);
  5485. const normalizedText = normalizeFieldMatchText(text);
  5486. if (!text || !normalizedText || normalizedValueSet.has(normalizedText)) return;
  5487. normalizedValueSet.add(normalizedText);
  5488. mergedValues.push(text);
  5489. });
  5490. return mergedValues.join(',');
  5491. };
  5492. const getRecognitionDirectionSampleValues = (fieldKey: string) => {
  5493. const mappedHeader = normalizeRuleText(effectiveFieldMapping.value[fieldKey] || fieldMapping.value[fieldKey]);
  5494. if (!mappedHeader) return [] as string[];
  5495. const matchedHeader = rightTableHeaders.value.find((item) => normalizeRuleText(item) === mappedHeader);
  5496. if (!matchedHeader) return [] as string[];
  5497. const collectedValues: string[] = [];
  5498. const normalizedValueSet = new Set<string>();
  5499. sourceTableRows.value.forEach((row) => {
  5500. const rawValue = normalizeRecognitionConfigText(row?.[matchedHeader]);
  5501. const normalizedValue = normalizeFieldMatchText(rawValue);
  5502. if (!rawValue || !normalizedValue || normalizedValueSet.has(normalizedValue)) return;
  5503. normalizedValueSet.add(normalizedValue);
  5504. collectedValues.push(rawValue);
  5505. });
  5506. return collectedValues;
  5507. };
  5508. const buildRecognitionDirectionEntriesByMappedColumn = (
  5509. field: Pick<RecognitionDirectionField, 'key' | 'label'>,
  5510. tab: RecognitionRuleTabKey,
  5511. entries: RecognitionDirectionItem[] = [],
  5512. ) => {
  5513. const baseEntries = entries.map((item, index) => ({
  5514. key: item.key || `${field.key}_${index}`,
  5515. label: normalizeRecognitionConfigText(item.label),
  5516. value: normalizeRecognitionConfigText(item.value),
  5517. }));
  5518. if (baseEntries.some((item) => item.label)) {
  5519. return baseEntries.filter((item) => item.label);
  5520. }
  5521. const sampleValues = getRecognitionDirectionSampleValues(field.key);
  5522. if (!sampleValues.length) {
  5523. return baseEntries.filter((item) => item.label);
  5524. }
  5525. const groupedKeywordMap = sampleValues.reduce(
  5526. (acc, value) => {
  5527. const groupLabel = normalizeRecognitionDirectionGroup(value, tab);
  5528. if (!groupLabel) return acc;
  5529. const coreKeyword = tab === '借贷标志' ? normalizeLoanFlagCoreValue(value) : normalizeCallTypeCoreValue(value);
  5530. const nextKeywords = [coreKeyword, value].filter((item): item is string => !!item);
  5531. acc[groupLabel] = mergeRecognitionDirectionKeywords((acc[groupLabel] || []).join(','), nextKeywords)
  5532. .split(/[\n,,;;、]+/)
  5533. .map((item) => item.trim())
  5534. .filter(Boolean);
  5535. return acc;
  5536. },
  5537. {} as Record<string, string[]>,
  5538. );
  5539. if (!Object.keys(groupedKeywordMap).length) {
  5540. return baseEntries.filter((item) => item.label);
  5541. }
  5542. const nextEntries = Object.keys(groupedKeywordMap).map((label, index) => ({
  5543. key: `${field.key}_${index}`,
  5544. label,
  5545. value: '',
  5546. }));
  5547. const groupEntryIndexMap = nextEntries.reduce(
  5548. (acc, item, index) => {
  5549. const groupLabel =
  5550. normalizeRecognitionDirectionGroup(item.label, tab) || normalizeRecognitionConfigText(item.label);
  5551. if (groupLabel && acc[groupLabel] === undefined) {
  5552. acc[groupLabel] = index;
  5553. }
  5554. return acc;
  5555. },
  5556. {} as Record<string, number>,
  5557. );
  5558. Object.entries(groupedKeywordMap).forEach(([groupLabel, keywords]) => {
  5559. let targetIndex = groupEntryIndexMap[groupLabel];
  5560. if (targetIndex === undefined) {
  5561. targetIndex = nextEntries.length;
  5562. nextEntries.push({
  5563. key: `${field.key}_${targetIndex}`,
  5564. label: groupLabel,
  5565. value: '',
  5566. });
  5567. groupEntryIndexMap[groupLabel] = targetIndex;
  5568. }
  5569. nextEntries[targetIndex].value = mergeRecognitionDirectionKeywords(nextEntries[targetIndex].value, keywords);
  5570. });
  5571. return nextEntries.filter((item) => item.label);
  5572. };
  5573. const buildRecognitionDirectionComparableSet = (
  5574. fields: RecognitionDirectionField[],
  5575. ): RecognitionDirectionComparableMap => {
  5576. const initialSet = {
  5577. 借贷标志: new Set<string>(),
  5578. 通信类别: new Set<string>(),
  5579. };
  5580. fields.forEach((field) => {
  5581. const currentSet = initialSet[field.tab];
  5582. field.entries.forEach((entry) => {
  5583. [entry.label, ...splitRecognitionDirectionKeywords(entry.value)].forEach((keyword) => {
  5584. const normalizedKeyword = normalizeRecognitionConfigText(keyword);
  5585. if (normalizedKeyword) {
  5586. currentSet.add(normalizedKeyword);
  5587. }
  5588. });
  5589. });
  5590. });
  5591. return initialSet;
  5592. };
  5593. const recognitionDirectionComparableSetByTab = computed<RecognitionDirectionComparableMap>(() =>
  5594. buildRecognitionDirectionComparableSet(recognitionDirectionFields.value),
  5595. );
  5596. const recognitionDirectionComparableSetByFieldKey = computed<Record<string, RecognitionDirectionComparableMap>>(() =>
  5597. recognitionDirectionFields.value.reduce(
  5598. (acc, field) => {
  5599. acc[field.key] = buildRecognitionDirectionComparableSet([field]);
  5600. return acc;
  5601. },
  5602. {} as Record<string, RecognitionDirectionComparableMap>,
  5603. ),
  5604. );
  5605. const matchRecognitionDirectionConfigValue = (value: string, tab: RecognitionRuleTabKey, targetKey = '') => {
  5606. const keywordSet =
  5607. (targetKey ? recognitionDirectionComparableSetByFieldKey.value[targetKey]?.[tab] : undefined) ||
  5608. recognitionDirectionComparableSetByTab.value[tab];
  5609. if (!keywordSet.size) return null;
  5610. const normalizedValue = normalizeRecognitionConfigText(value);
  5611. if (normalizedValue && keywordSet.has(normalizedValue)) {
  5612. return true;
  5613. }
  5614. return false;
  5615. };
  5616. const recognitionHeaderPreviewText = computed(() => {
  5617. if (headerRowIndex.value === null) {
  5618. return '未设置表头行,暂无法分析表头上方内容。';
  5619. }
  5620. const previewHeaders = rightTableHeaders.value.filter((header) => !addedColumns.value.includes(header));
  5621. const previewRows = sourceTableRows.value.slice(0, headerRowIndex.value);
  5622. const content = previewRows
  5623. .map((row) =>
  5624. previewHeaders
  5625. .map((header) => normalizeSingleLineText(row[header]))
  5626. .filter(Boolean)
  5627. .join(''),
  5628. )
  5629. .filter(Boolean)
  5630. .join('');
  5631. return content || '当前表头行上方暂无可识别内容。';
  5632. });
  5633. const recognitionHeaderPreviewMeta = computed(() => {
  5634. if (headerRowIndex.value === null) return '待识别';
  5635. if (headerRowIndex.value <= 0) return '上方 0 行';
  5636. return `上方 ${headerRowIndex.value} 行`;
  5637. });
  5638. const normalizeRecognitionBasicInfoText = (value: unknown) => normalizeRecognitionConfigText(value).trim();
  5639. const stripRecognitionBasicInfoTrailingColon = (value: string) => value.replace(/[::]\s*$/g, '').trim();
  5640. const normalizeRecognitionBasicInfoStorageText = (value: unknown) =>
  5641. stripRecognitionBasicInfoTrailingColon(normalizeRecognitionBasicInfoText(value));
  5642. const resolveRecognitionBasicInfoTargetField = (value: unknown) => {
  5643. const normalizedValue = normalizeRecognitionBasicInfoStorageText(value);
  5644. if (!normalizedValue) return '';
  5645. return normalizedFixedRecognitionBasicInfoFieldMap[normalizeFieldMatchText(normalizedValue)] || normalizedValue;
  5646. };
  5647. const parseRecognitionBasicInfoPayload = (funcRegx: unknown) => {
  5648. const rawText = normalizeRecognitionBasicInfoText(funcRegx);
  5649. if (!rawText) return null;
  5650. const matchedText = rawText.match(recognitionBasicInfoRegex)?.[0];
  5651. if (!matchedText) return null;
  5652. try {
  5653. const parsedValue = JSON.parse(matchedText) as Record<string, any>;
  5654. if (!parsedValue || typeof parsedValue !== 'object' || Array.isArray(parsedValue)) {
  5655. return null;
  5656. }
  5657. return parsedValue;
  5658. } catch (error) {
  5659. console.error('解析基本信息配置失败', error);
  5660. return null;
  5661. }
  5662. };
  5663. const buildRecognitionBasicInfoTargetFieldByHeaderMatchMap = (funcRegx: unknown) => {
  5664. const parsedValue = parseRecognitionBasicInfoPayload(funcRegx);
  5665. if (!parsedValue) return {} as Record<string, string>;
  5666. return Object.entries(parsedValue).reduce(
  5667. (acc, [targetField, rawValue]) => {
  5668. const rawList = Array.isArray(rawValue) ? rawValue : [];
  5669. const headerMatch = normalizeRecognitionBasicInfoText(rawList[0] ?? '');
  5670. const normalizedHeaderMatch = normalizeRecognitionBasicInfoStorageText(headerMatch);
  5671. if (normalizedHeaderMatch) {
  5672. acc[normalizedHeaderMatch] = resolveRecognitionBasicInfoTargetField(targetField);
  5673. }
  5674. return acc;
  5675. },
  5676. {} as Record<string, string>,
  5677. );
  5678. };
  5679. const parseRecognitionBasicInfoRows = (funcRegx: unknown, referenceFuncRegx?: unknown): RecognitionBasicInfoRow[] => {
  5680. const parsedValue = parseRecognitionBasicInfoPayload(funcRegx);
  5681. if (!parsedValue) return [];
  5682. const parentTargetFieldMap = buildRecognitionBasicInfoTargetFieldByHeaderMatchMap(referenceFuncRegx);
  5683. return Object.entries(parsedValue).reduce((acc, [storedKey, rawValue]) => {
  5684. const rawList = Array.isArray(rawValue) ? rawValue : [];
  5685. const headerMatch = normalizeRecognitionBasicInfoText(rawList[0] ?? '');
  5686. const normalizedHeaderMatch = normalizeRecognitionBasicInfoStorageText(headerMatch);
  5687. const endFlag = normalizeRecognitionBasicInfoText(rawList[1] ?? '');
  5688. const normalizedStoredKey = normalizeRecognitionBasicInfoStorageText(storedKey);
  5689. const resolvedStoredTargetField = resolveRecognitionBasicInfoTargetField(normalizedStoredKey);
  5690. const mappedParentTargetField =
  5691. normalizedHeaderMatch && normalizedStoredKey === normalizedHeaderMatch
  5692. ? parentTargetFieldMap[normalizedHeaderMatch]
  5693. : '';
  5694. const resolvedTargetField = mappedParentTargetField || resolvedStoredTargetField;
  5695. acc.push({
  5696. key: createRecognitionBasicInfoKey(),
  5697. headerMatch,
  5698. targetField: resolvedTargetField,
  5699. endFlag,
  5700. });
  5701. return acc;
  5702. }, [] as RecognitionBasicInfoRow[]);
  5703. };
  5704. const clearRecognitionBasicInfoFilledContent = (rows: RecognitionBasicInfoRow[]) =>
  5705. rows.map((item) => ({
  5706. ...item,
  5707. headerMatch: '',
  5708. endFlag: '',
  5709. }));
  5710. const hasRecognitionBasicInfoFilledContent = (rows: RecognitionBasicInfoRow[]) =>
  5711. rows.some(
  5712. (item) =>
  5713. !!normalizeRecognitionBasicInfoText(item.headerMatch) || !!normalizeRecognitionBasicInfoText(item.endFlag),
  5714. );
  5715. const hasOwnRecognitionBasicInfoConfig = (node?: TreeFileInfo | null) =>
  5716. !!node && !!normalizeRecognitionConfigText(node.funcRegx);
  5717. const isSameRecognitionBasicInfoConfig = (left?: unknown, right?: unknown) => {
  5718. const leftText = normalizeRecognitionConfigText(left);
  5719. const rightText = normalizeRecognitionConfigText(right);
  5720. return !!leftText && !!rightText && leftText === rightText;
  5721. };
  5722. const resolveRecognitionBasicInfoSource = (
  5723. node?: TreeFileInfo | null,
  5724. options: { preferTemplate?: boolean } = {},
  5725. ) => {
  5726. const templateFuncRegx = normalizeRecognitionConfigText(
  5727. templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
  5728. );
  5729. const hasNodeFuncRegx = hasOwnRecognitionBasicInfoConfig(node);
  5730. const nodeFuncRegx = normalizeRecognitionConfigText(node?.funcRegx);
  5731. if (options.preferTemplate) {
  5732. return templateFuncRegx;
  5733. }
  5734. if (hasNodeFuncRegx) {
  5735. return nodeFuncRegx;
  5736. }
  5737. return templateFuncRegx;
  5738. };
  5739. const applyRecognitionBasicInfoState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  5740. const templateId = normalizeTemplateId(
  5741. node?.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  5742. );
  5743. const parentTemplateId = getSelectedTemplateMainId(node?.mainId);
  5744. const parentTemplate = parentTemplateId
  5745. ? resolveTemplateInfoById(parentTemplateId)
  5746. : resolveParentTemplateInfoById(templateId);
  5747. const source = resolveRecognitionBasicInfoSource(node, options);
  5748. const hasNodeFuncRegx =
  5749. hasOwnRecognitionBasicInfoConfig(node) &&
  5750. !isSameRecognitionBasicInfoConfig(
  5751. node?.funcRegx,
  5752. templateDetail.value?.funcRegx || activeTemplateItem.value?.funcRegx,
  5753. ) &&
  5754. !isSameRecognitionBasicInfoConfig(node?.funcRegx, parentTemplate?.funcRegx);
  5755. const shouldClearFilledContent = options.preferTemplate || !hasNodeFuncRegx;
  5756. recognitionBasicInfoBaseFuncRegx.value = source;
  5757. const parsedRows = parseRecognitionBasicInfoRows(source, parentTemplate?.funcRegx);
  5758. recognitionBasicInfoRows.value = shouldClearFilledContent
  5759. ? clearRecognitionBasicInfoFilledContent(parsedRows)
  5760. : parsedRows;
  5761. recognitionBasicInfoExpanded.value = !!recognitionBasicInfoRows.value.length;
  5762. };
  5763. const resolveRecognitionAmountZfEnabled = (
  5764. node?: TreeFileInfo | null,
  5765. options: { preferTemplate?: boolean } = {},
  5766. ) => {
  5767. const tableRule = findTransAmountTableRule(node?.tableRuleList);
  5768. const tableRuleZf = getRecordZfValue(tableRule);
  5769. const templateField = findTransAmountTemplateFieldMeta(templateDetail.value || activeTemplateItem.value);
  5770. const templateZf = getRecordZfValue(templateField);
  5771. if (options.preferTemplate) {
  5772. return templateZf !== undefined
  5773. ? getZfFlag(templateZf)
  5774. : tableRuleZf !== undefined
  5775. ? getZfFlag(tableRuleZf)
  5776. : false;
  5777. }
  5778. return tableRuleZf !== undefined
  5779. ? getZfFlag(tableRuleZf)
  5780. : templateZf !== undefined
  5781. ? getZfFlag(templateZf)
  5782. : false;
  5783. };
  5784. const applyRecognitionAmountZfState = (node?: TreeFileInfo | null, options: { preferTemplate?: boolean } = {}) => {
  5785. recognitionAmountZfEnabled.value = resolveRecognitionAmountZfEnabled(node, options);
  5786. };
  5787. const handleRecognitionAmountZfChange = (checked: unknown) => {
  5788. const enabled = getZfFlag(checked);
  5789. recognitionAmountZfEnabled.value = enabled;
  5790. if (mappingPopoverOpenKey.value && isTargetHandledByAmountZf(mappingPopoverOpenKey.value)) {
  5791. mappingPopoverOpenKey.value = '';
  5792. originFilterKeyword.value = '';
  5793. }
  5794. saveCurrentNodeState();
  5795. runRulePipeline();
  5796. createMessage.success(enabled ? '正负标识识别已开启' : '正负标识识别已关闭');
  5797. };
  5798. const buildRecognitionBasicInfoFuncRegx = () => {
  5799. if (!recognitionBasicInfoRows.value.length) {
  5800. return recognitionBasicInfoBaseFuncRegx.value;
  5801. }
  5802. if (!hasRecognitionBasicInfoFilledContent(recognitionBasicInfoRows.value)) {
  5803. return '';
  5804. }
  5805. const payload = recognitionBasicInfoRows.value.reduce(
  5806. (acc, item) => {
  5807. const targetField = resolveRecognitionBasicInfoTargetField(item.targetField);
  5808. if (!targetField) return acc;
  5809. acc[targetField] = [
  5810. normalizeRecognitionBasicInfoText(item.headerMatch),
  5811. normalizeRecognitionBasicInfoText(item.endFlag),
  5812. ];
  5813. return acc;
  5814. },
  5815. {} as Record<string, string[]>,
  5816. );
  5817. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  5818. };
  5819. const handleClearRecognitionDirection = () => {
  5820. const currentTab = activeRecognitionRule.value as RecognitionRuleTabKey;
  5821. if (!currentTab) return;
  5822. recognitionDirectionFields.value = recognitionDirectionFields.value.map((item) => {
  5823. if (item.tab !== currentTab) return item;
  5824. return {
  5825. ...item,
  5826. entries: item.entries.map((entry) => ({
  5827. ...entry,
  5828. value: '',
  5829. })),
  5830. };
  5831. });
  5832. };
  5833. const handleSaveRecognitionDirection = () => {
  5834. if (!activeRecognitionDirectionFields.value.length) return;
  5835. saveCurrentNodeState();
  5836. runRulePipeline();
  5837. createMessage.success(`${activeRecognitionRule.value}配置已保存`);
  5838. };
  5839. const refreshRecognitionDirectionState = (node?: TreeFileInfo | null) => {
  5840. recognitionDirectionFields.value = resolveRecognitionDirectionFields(node, {
  5841. currentFields: recognitionDirectionFields.value,
  5842. });
  5843. };
  5844. const getMappingStatusClass = (item: Pick<MappingTargetField, 'key' | 'label' | 'required'>) => {
  5845. if (isTargetMatched(item.key)) return 'is-matched';
  5846. if (isMappingTargetRequired(item)) return 'is-required';
  5847. return 'is-optional';
  5848. };
  5849. const getFieldMatchScore = (targetLabel: string, header: string) => {
  5850. const rawTarget = normalizeRuleText(targetLabel);
  5851. const rawHeader = normalizeRuleText(header);
  5852. if (!rawTarget || !rawHeader) return -1;
  5853. if (rawTarget === rawHeader) return 1000;
  5854. const normalizedTarget = normalizeFieldMatchText(rawTarget);
  5855. const normalizedHeader = normalizeFieldMatchText(rawHeader);
  5856. if (!normalizedTarget || !normalizedHeader) return -1;
  5857. if (normalizedTarget === normalizedHeader) return 900;
  5858. return -1;
  5859. };
  5860. const findBestMatchedHeader = (targetLabel: string, headers: string[], usedHeaders: Set<string>) => {
  5861. let bestHeader = '';
  5862. let bestScore = -1;
  5863. headers.forEach((header, index) => {
  5864. if (usedHeaders.has(header)) return;
  5865. const score = getFieldMatchScore(targetLabel, header);
  5866. if (score < 0) return;
  5867. if (score > bestScore) {
  5868. bestScore = score;
  5869. bestHeader = header;
  5870. return;
  5871. }
  5872. if (score === bestScore && bestHeader) {
  5873. const currentDiff = Math.abs(
  5874. normalizeFieldMatchText(bestHeader).length - normalizeFieldMatchText(targetLabel).length,
  5875. );
  5876. const nextDiff = Math.abs(normalizeFieldMatchText(header).length - normalizeFieldMatchText(targetLabel).length);
  5877. if (nextDiff < currentDiff) {
  5878. bestHeader = header;
  5879. return;
  5880. }
  5881. if (nextDiff === currentDiff) {
  5882. const currentIndex = headers.findIndex((item) => item === bestHeader);
  5883. if (currentIndex < 0 || index < currentIndex) {
  5884. bestHeader = header;
  5885. }
  5886. }
  5887. }
  5888. });
  5889. return bestHeader;
  5890. };
  5891. const autoMatchFields = (showMessage = true) => {
  5892. const headers = [...rightTableHeaders.value];
  5893. const usedHeaders = new Set<string>();
  5894. const lockedMapping = Object.entries(fieldMapping.value).reduce(
  5895. (acc, [targetKey, header]) => {
  5896. if (isTargetHandledByRecognitionRule(targetKey) && header) {
  5897. acc[targetKey] = header;
  5898. }
  5899. return acc;
  5900. },
  5901. {} as Record<string, string>,
  5902. );
  5903. const nextMapping = fixedTargetFields.value.reduce((acc, item) => {
  5904. if (isTargetHandledByRecognitionRule(item.key)) {
  5905. return acc;
  5906. }
  5907. const bestHeader = findBestMatchedHeader(item.label, headers, usedHeaders);
  5908. acc[item.key] = bestHeader || '';
  5909. if (bestHeader) {
  5910. usedHeaders.add(bestHeader);
  5911. }
  5912. return acc;
  5913. }, lockedMapping);
  5914. fieldMapping.value = nextMapping;
  5915. refreshRecognitionDirectionState(activeNodeInfo.value);
  5916. if (showMessage) {
  5917. const matchedCount = fixedTargetFields.value.filter(
  5918. (item) => isTargetHandledByRecognitionRule(item.key) || !!nextMapping[item.key],
  5919. ).length;
  5920. createMessage.success(`已完成智能预匹配,共匹配 ${matchedCount} 项`);
  5921. }
  5922. };
  5923. const clearFieldMapping = () => {
  5924. fieldMapping.value = {};
  5925. refreshRecognitionDirectionState(activeNodeInfo.value);
  5926. };
  5927. const applyNodePreset = async (item?: TreeFileInfo | null) => {
  5928. const previewDataset = await loadPreviewDatasetFromFile(item);
  5929. const hasPresetHeaders = previewDataset ? applyPreviewDataset(previewDataset) : applyPresetHeadersFromNode(item);
  5930. restoreStoredColumnRules(item?.tableRuleList);
  5931. applyRecognitionDirectionState(item);
  5932. applyRecognitionBasicInfoState(item);
  5933. applyRecognitionAmountZfState(item);
  5934. childTemplateName.value = normalizeRuleText(item?.childTemplateName);
  5935. const headerRow = normalizeStoredLineNoToRowIndex(item?.lineNo);
  5936. if (headerRow !== null && !hasPresetHeaders) {
  5937. applyHeaderRow(headerRow, { silent: true, autoMatch: false });
  5938. } else {
  5939. headerRowIndex.value = headerRow;
  5940. selectedRowIndex.value = headerRow;
  5941. syncHeaderRowInput();
  5942. }
  5943. if (!applyTableRuleMapping(item?.tableRuleList)) {
  5944. autoMatchFields(false);
  5945. }
  5946. restoreStoredFunRules(item?.tableRuleList);
  5947. restoreFileLevelRules(item);
  5948. runRulePipeline();
  5949. };
  5950. type TemplateTab = 'template' | 'plugin';
  5951. const DEFAULT_TEMPLATE_CATEGORY = '全部分类';
  5952. const FALLBACK_TEMPLATE_CATEGORY = '未分类';
  5953. const templateModalOpen = ref(false);
  5954. const activeTemplateTab = ref<TemplateTab>('template');
  5955. const activeCategory = ref(DEFAULT_TEMPLATE_CATEGORY);
  5956. const templateKeyword = ref('');
  5957. const templateSource = ref<number>(1);
  5958. const templateList = ref<TemplateListItem[]>(mergeTemplateCollections(mockTemplateList));
  5959. const templateCategoryOptions = ref<string[]>([DEFAULT_TEMPLATE_CATEGORY]);
  5960. const activeTemplateItem = ref<TemplateListItem | null>(null);
  5961. const modalTemplateItem = ref<TemplateListItem | null>(null);
  5962. const modalTemplateDetail = ref<TableInfo | null>(null);
  5963. const templatePreviewLoading = ref(false);
  5964. const templateListLoading = ref(false);
  5965. const templateListLoaded = ref(templateList.value.length > 0);
  5966. const templateDetailCache = new Map<number, TableInfo>();
  5967. let templatePreviewRequestToken = 0;
  5968. const templateSourceOptions = [
  5969. { label: '全部', value: 0 },
  5970. { label: '系统内置', value: 1 },
  5971. { label: '新创建子模板', value: 2 },
  5972. ];
  5973. const categories = computed(() => {
  5974. const set = new Set<string>(
  5975. templateCategoryOptions.value.length ? templateCategoryOptions.value : [DEFAULT_TEMPLATE_CATEGORY],
  5976. );
  5977. templateList.value.forEach((t) => set.add(t.classify || FALLBACK_TEMPLATE_CATEGORY));
  5978. return Array.from(set);
  5979. });
  5980. const pluginList = ref<TemplateListItem[]>([
  5981. {
  5982. id: -1,
  5983. tableNameCn: '工行账单特殊处理',
  5984. tableNameEn: 'plug',
  5985. classify: '金融类',
  5986. headers: '插件模板',
  5987. } as TableInfo,
  5988. ]);
  5989. const pluginCategorySet = computed(() => {
  5990. const set = new Set<string>();
  5991. pluginList.value.forEach((item) => {
  5992. const category = normalizeTemplateClassifyText(item.classify) || FALLBACK_TEMPLATE_CATEGORY;
  5993. set.add(category);
  5994. });
  5995. return set;
  5996. });
  5997. const showTemplateTabSwitch = computed(
  5998. () => activeCategory.value === DEFAULT_TEMPLATE_CATEGORY || pluginCategorySet.value.has(activeCategory.value),
  5999. );
  6000. const isPluginTemplateTab = computed(() => activeTemplateTab.value === 'plugin');
  6001. const isSystemTemplateRecord = (record?: Partial<TemplateListItem> | null) => {
  6002. const normalizedValue = String(record?.hasInner ?? '')
  6003. .trim()
  6004. .toUpperCase();
  6005. return ['YES', '1', 'TRUE'].includes(normalizedValue);
  6006. };
  6007. const isChildTemplateRecord = (record?: Partial<TemplateListItem> | null) =>
  6008. !isSystemTemplateRecord(record) && normalizeTemplateId(record?.mainId) > 0;
  6009. const templateSearchBaseList = computed(() => {
  6010. const baseList = activeTemplateTab.value === 'plugin' ? pluginList.value : templateList.value;
  6011. return baseList.filter((tpl) => {
  6012. const passKeyword =
  6013. !templateKeyword.value || `${tpl.tableNameCn || ''}${tpl.tableNameEn || ''}`.includes(templateKeyword.value);
  6014. const passSource =
  6015. templateSource.value === 0 ||
  6016. (templateSource.value === 1 && isSystemTemplateRecord(tpl)) ||
  6017. (templateSource.value === 2 && isChildTemplateRecord(tpl));
  6018. return passKeyword && passSource;
  6019. });
  6020. });
  6021. const filteredTemplateList = computed(() => {
  6022. return templateSearchBaseList.value.filter((tpl) => {
  6023. return (
  6024. activeTemplateTab.value === 'plugin' ||
  6025. activeCategory.value === DEFAULT_TEMPLATE_CATEGORY ||
  6026. (tpl.classify || FALLBACK_TEMPLATE_CATEGORY) === activeCategory.value
  6027. );
  6028. });
  6029. });
  6030. const templateCategoryCountMap = computed<Record<string, number>>(() => {
  6031. return templateSearchBaseList.value.reduce(
  6032. (acc, tpl) => {
  6033. const category = tpl.classify || FALLBACK_TEMPLATE_CATEGORY;
  6034. acc[category] = (acc[category] || 0) + 1;
  6035. acc[DEFAULT_TEMPLATE_CATEGORY] = (acc[DEFAULT_TEMPLATE_CATEGORY] || 0) + 1;
  6036. return acc;
  6037. },
  6038. {} as Record<string, number>,
  6039. );
  6040. });
  6041. const getTemplateDisplayName = (tpl?: Partial<TemplateListItem> | null) => {
  6042. return tpl?.tableNameCn || tpl?.tableNameEn || '未命名模板';
  6043. };
  6044. const getTemplateSourceLabel = (tpl?: Partial<TemplateListItem> | null) => {
  6045. if (Number(tpl?.id) < 0) return '模板插件';
  6046. return isSystemTemplateRecord(tpl) ? '系统内置' : '新创建子模板';
  6047. };
  6048. const getTemplateHeaderList = (tpl?: Partial<TableInfo> | null) => normalizeTemplateHeaders((tpl as any)?.headers);
  6049. const getTemplateHeaderCount = (tpl?: Partial<TableInfo> | null) => getTemplateHeaderList(tpl).length;
  6050. const getCategoryCount = (category: string) => {
  6051. if (category === DEFAULT_TEMPLATE_CATEGORY) return templateCategoryCountMap.value[DEFAULT_TEMPLATE_CATEGORY] || 0;
  6052. return templateCategoryCountMap.value[category] || 0;
  6053. };
  6054. const resolveTemplateClassifyRecords = (payload: unknown): unknown[] => {
  6055. if (Array.isArray(payload)) return payload;
  6056. if (!payload || typeof payload !== 'object') return [];
  6057. const record = payload as Record<string, unknown>;
  6058. for (const key of ['data', 'list', 'records', 'result']) {
  6059. const value = record[key];
  6060. if (Array.isArray(value)) return value;
  6061. }
  6062. return [];
  6063. };
  6064. const normalizeTemplateClassifyValue = (item: unknown) => {
  6065. if (item === null || item === undefined) return '';
  6066. if (typeof item === 'string' || typeof item === 'number') {
  6067. return normalizeTemplateClassifyText(item);
  6068. }
  6069. if (typeof item !== 'object') return '';
  6070. const record = item as Record<string, unknown>;
  6071. return normalizeTemplateClassifyText(
  6072. record.value ?? record.classify ?? record.name ?? record.label ?? record.dictValue ?? record.dictLabel,
  6073. );
  6074. };
  6075. const loadTemplateCategories = async () => {
  6076. try {
  6077. const response = await tplCategoryList();
  6078. const categorySet = new Set<string>([DEFAULT_TEMPLATE_CATEGORY]);
  6079. resolveTemplateClassifyRecords(response).forEach((item) => {
  6080. const value = normalizeTemplateClassifyValue(item);
  6081. if (value && value !== DEFAULT_TEMPLATE_CATEGORY) {
  6082. categorySet.add(value);
  6083. }
  6084. });
  6085. templateCategoryOptions.value = Array.from(categorySet);
  6086. } catch (error) {
  6087. templateCategoryOptions.value = [DEFAULT_TEMPLATE_CATEGORY];
  6088. }
  6089. };
  6090. const getTemplateListItemById = (templateId?: string | number | null) => {
  6091. const normalizedId = normalizeTemplateId(templateId);
  6092. if (!normalizedId) return null;
  6093. return templateList.value.find((item) => normalizeTemplateId(item.id) === normalizedId) || null;
  6094. };
  6095. const applyTableRuleRequiredFlags = (fields: MappingTargetField[], tableRules: TreeTableRuleInfo[]) => {
  6096. if (!tableRules.length) return fields;
  6097. const ruleMapByKey = tableRules.reduce(
  6098. (acc, item, index) => {
  6099. const key = getTableRuleTargetKey(item, index);
  6100. if (key) acc[key] = item;
  6101. return acc;
  6102. },
  6103. {} as Record<string, TreeTableRuleInfo>,
  6104. );
  6105. const ruleMapByLabel = tableRules.reduce(
  6106. (acc, item, index) => {
  6107. const label = getTableRuleTargetLabel(item, index);
  6108. if (label) acc[label] = item;
  6109. return acc;
  6110. },
  6111. {} as Record<string, TreeTableRuleInfo>,
  6112. );
  6113. return fields.map((item) => {
  6114. const rule = ruleMapByKey[item.key] || ruleMapByLabel[item.label];
  6115. return {
  6116. ...item,
  6117. required: rule ? getTableRuleRequiredFlag(rule, item.required) : item.required,
  6118. };
  6119. });
  6120. };
  6121. const resolveNodeTargetFields = (item?: TreeFileInfo | null, preferredTemplateId?: string | number | null) => {
  6122. const tableRules = normalizeTableRuleList(item?.tableRuleList);
  6123. const templateId = normalizeTemplateId(preferredTemplateId ?? item?.templateId);
  6124. const template = (templateId && (getTemplateListItemById(templateId) || mockTemplateMap[templateId])) || null;
  6125. const baseFields = tableRules.length
  6126. ? buildTargetFieldsFromRuleList(tableRules)
  6127. : buildTargetFieldsFromTemplateInfo(template);
  6128. return applyTableRuleRequiredFlags(baseFields, tableRules);
  6129. };
  6130. const getRequiredMatchStats = (
  6131. fields: MappingTargetField[],
  6132. mapping: Record<string, string>,
  6133. recognitionRows: RecognitionBasicInfoRow[] = [],
  6134. amountZfEnabled = false,
  6135. ) => {
  6136. const matchedByRecognitionBasicInfo = buildRecognitionBasicInfoMatchedTargetKeySet(recognitionRows, fields);
  6137. const handledTargetKeySet = new Set(matchedByRecognitionBasicInfo);
  6138. buildAmountZfHandledTargetKeySet(fields, amountZfEnabled).forEach((key) => {
  6139. handledTargetKeySet.add(key);
  6140. });
  6141. const effectiveMapping = buildEffectiveFieldMapping(mapping, handledTargetKeySet);
  6142. const requiredFields = fields.filter((item) => item.required);
  6143. const requiredTotal = requiredFields.length;
  6144. const requiredMatched = requiredFields.filter(
  6145. (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
  6146. ).length;
  6147. const matchedCount = fields.filter(
  6148. (item) => handledTargetKeySet.has(item.key) || !!effectiveMapping[item.key],
  6149. ).length;
  6150. return { requiredTotal, requiredMatched, matchedCount };
  6151. };
  6152. const getSheetNodeVisualState = (item?: TreeFileInfo | null): SheetNodeVisualState => {
  6153. if (!isTreeSheetNode(item)) return 'fail';
  6154. const nodeKey = getTreeNodeKey(item);
  6155. if (analyzingNodeKeys.value.includes(nodeKey)) {
  6156. return 'analyzing';
  6157. }
  6158. const defaultState = getFileStatusVisualState(item.fileStatus) || (isSheetMatchComplete(item) ? 'success' : 'fail');
  6159. const initialSignature = initialNodeStatusSignatureMap.get(nodeKey);
  6160. if (activeNodeKey.value === nodeKey) {
  6161. const currentState = buildNodeCacheState();
  6162. const hasEditedState = !initialSignature || buildNodeStatusSignature(currentState) !== initialSignature;
  6163. if (!hasEditedState) {
  6164. return defaultState;
  6165. }
  6166. return isMatchSuccessful(requiredTotal.value, requiredMatchedCount.value, matchedFieldCount.value)
  6167. ? 'success'
  6168. : 'fail';
  6169. }
  6170. const cached = nodeStateCache.get(nodeKey);
  6171. if (cached) {
  6172. const hasEditedState = !initialSignature || buildNodeStatusSignature(cached) !== initialSignature;
  6173. if (!hasEditedState) {
  6174. return defaultState;
  6175. }
  6176. const {
  6177. requiredTotal: cachedRequiredTotal,
  6178. requiredMatched: cachedRequiredMatched,
  6179. matchedCount: cachedMatchedCount,
  6180. } = getRequiredMatchStats(
  6181. resolveNodeTargetFields(item, cached.selectedTemplateId),
  6182. cached.fieldMapping,
  6183. cached.recognitionBasicInfoRows,
  6184. cached.recognitionAmountZfEnabled,
  6185. );
  6186. return isMatchSuccessful(cachedRequiredTotal, cachedRequiredMatched, cachedMatchedCount) ? 'success' : 'fail';
  6187. }
  6188. return defaultState;
  6189. };
  6190. const mergeTemplateListItem = (template?: TemplateListItem | TableInfo | null) => {
  6191. const templateId = normalizeTemplateId(template?.id);
  6192. if (!template || !templateId) return;
  6193. const nextItem = template as TemplateListItem;
  6194. const targetIndex = templateList.value.findIndex((item) => normalizeTemplateId(item.id) === templateId);
  6195. if (targetIndex >= 0) {
  6196. templateList.value[targetIndex] = {
  6197. ...templateList.value[targetIndex],
  6198. ...nextItem,
  6199. };
  6200. return;
  6201. }
  6202. templateList.value = [...templateList.value, nextItem];
  6203. };
  6204. const syncActiveTemplateItem = (preferredId?: string | number | null) => {
  6205. const preferredTemplateId = normalizeTemplateId(preferredId);
  6206. const currentTemplateId = normalizeTemplateId(activeTemplateItem.value?.id);
  6207. const currentBaseList = activeTemplateTab.value === 'plugin' ? pluginList.value : filteredTemplateList.value;
  6208. if (preferredTemplateId) {
  6209. const matched =
  6210. currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) ||
  6211. templateList.value.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) ||
  6212. null;
  6213. if (matched) {
  6214. activeTemplateItem.value = matched;
  6215. return matched;
  6216. }
  6217. }
  6218. if (currentTemplateId) {
  6219. const currentMatched =
  6220. currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) ||
  6221. templateList.value.find((item) => normalizeTemplateId(item.id) === currentTemplateId) ||
  6222. null;
  6223. if (currentMatched) {
  6224. activeTemplateItem.value = currentMatched;
  6225. return currentMatched;
  6226. }
  6227. }
  6228. activeTemplateItem.value = null;
  6229. return activeTemplateItem.value;
  6230. };
  6231. const syncModalTemplateItem = (preferredId?: string | number | null) => {
  6232. const currentBaseList = filteredTemplateList.value;
  6233. if (activeTemplateTab.value === 'plugin') {
  6234. const currentPluginId = modalTemplateItem.value?.id;
  6235. const matchedPlugin = currentBaseList.find((item) => String(item.id) === String(currentPluginId)) || null;
  6236. modalTemplateItem.value = matchedPlugin;
  6237. return modalTemplateItem.value;
  6238. }
  6239. const preferredTemplateId = normalizeTemplateId(preferredId);
  6240. const currentTemplateId = normalizeTemplateId(modalTemplateItem.value?.id);
  6241. if (preferredTemplateId) {
  6242. const matched = currentBaseList.find((item) => normalizeTemplateId(item.id) === preferredTemplateId) || null;
  6243. if (matched) {
  6244. modalTemplateItem.value = matched;
  6245. return matched;
  6246. }
  6247. }
  6248. if (currentTemplateId) {
  6249. const currentMatched = currentBaseList.find((item) => normalizeTemplateId(item.id) === currentTemplateId) || null;
  6250. if (currentMatched) {
  6251. modalTemplateItem.value = currentMatched;
  6252. return currentMatched;
  6253. }
  6254. }
  6255. modalTemplateItem.value = currentBaseList[0] || null;
  6256. return modalTemplateItem.value;
  6257. };
  6258. const mergeTemplateInfo = (
  6259. base?: Partial<TemplateListItem> | Partial<TableInfo> | null,
  6260. detail?: Partial<TableInfo> | null,
  6261. ) => {
  6262. if (!base && !detail) return null;
  6263. return { ...(base || {}), ...(detail || {}) } as TemplateListItem & TableInfo;
  6264. };
  6265. const resolveTemplateInfoById = (templateId?: string | number | null) => {
  6266. const id = normalizeTemplateId(templateId);
  6267. if (!id) return null;
  6268. const activeDetail = normalizeTemplateId(templateDetail.value?.id) === id ? templateDetail.value : null;
  6269. const activeItem = normalizeTemplateId(activeTemplateItem.value?.id) === id ? activeTemplateItem.value : null;
  6270. const cachedDetail = templateDetailCache.get(id) || null;
  6271. const listItem = getTemplateListItemById(id) || mockTemplateMap[id] || null;
  6272. return (
  6273. mergeTemplateInfo(activeItem || listItem || activeDetail, activeDetail || cachedDetail) ||
  6274. activeDetail ||
  6275. cachedDetail ||
  6276. activeItem ||
  6277. listItem ||
  6278. null
  6279. );
  6280. };
  6281. const resolveParentTemplateInfoById = (templateId?: string | number | null) => {
  6282. const template = resolveTemplateInfoById(templateId);
  6283. const mainId = normalizeTemplateId((template as TableInfo | null)?.mainId);
  6284. return (mainId && resolveTemplateInfoById(mainId)) || template;
  6285. };
  6286. const getSelectedTemplateMainId = (fallbackMainId?: string | number | null) => {
  6287. if (templateDetail.value && Object.prototype.hasOwnProperty.call(templateDetail.value, 'mainId')) {
  6288. return normalizeTemplateId(templateDetail.value.mainId);
  6289. }
  6290. if (activeTemplateItem.value && Object.prototype.hasOwnProperty.call(activeTemplateItem.value, 'mainId')) {
  6291. return normalizeTemplateId(activeTemplateItem.value.mainId);
  6292. }
  6293. return normalizeTemplateId(fallbackMainId);
  6294. };
  6295. const syncModalTemplatePreviewState = (
  6296. template?: TemplateListItem | null,
  6297. options: { preferAppliedDetail?: boolean } = {},
  6298. ) => {
  6299. modalTemplateItem.value = template || null;
  6300. modalTemplateDetail.value = null;
  6301. templatePreviewLoading.value = false;
  6302. templatePreviewRequestToken += 1;
  6303. const id = normalizeTemplateId(template?.id);
  6304. if (!id || Number(template?.id) < 0) return null;
  6305. if (options.preferAppliedDetail && normalizeTemplateId(templateDetail.value?.id) === id && templateDetail.value) {
  6306. modalTemplateDetail.value = templateDetail.value;
  6307. return modalTemplateDetail.value;
  6308. }
  6309. const cachedDetail = templateDetailCache.get(id);
  6310. if (cachedDetail) {
  6311. modalTemplateDetail.value = cachedDetail;
  6312. return modalTemplateDetail.value;
  6313. }
  6314. return null;
  6315. };
  6316. const loadTemplatePreviewDetail = async (
  6317. template?: TemplateListItem | null,
  6318. options: { preferAppliedDetail?: boolean } = {},
  6319. ) => {
  6320. const existingDetail = syncModalTemplatePreviewState(template, options);
  6321. if (existingDetail) {
  6322. return existingDetail;
  6323. }
  6324. const id = normalizeTemplateId(template?.id);
  6325. if (!id || Number(template?.id) < 0) return null;
  6326. const requestToken = ++templatePreviewRequestToken;
  6327. templatePreviewLoading.value = true;
  6328. try {
  6329. const detail = await tplDetail({ id });
  6330. if (requestToken !== templatePreviewRequestToken) return null;
  6331. if (detail) {
  6332. templateDetailCache.set(id, detail);
  6333. modalTemplateDetail.value = detail;
  6334. mergeTemplateListItem(mergeTemplateInfo(template, detail));
  6335. modalTemplateItem.value = getTemplateListItemById(id) || template || null;
  6336. }
  6337. return modalTemplateDetail.value;
  6338. } catch (error) {
  6339. if (requestToken === templatePreviewRequestToken) {
  6340. modalTemplateDetail.value = null;
  6341. }
  6342. return null;
  6343. } finally {
  6344. if (requestToken === templatePreviewRequestToken) {
  6345. templatePreviewLoading.value = false;
  6346. }
  6347. }
  6348. };
  6349. const handleTemplateItemClick = async (template: TemplateListItem) => {
  6350. if (activeTemplateTab.value === 'plugin' || Number(template.id) < 0) {
  6351. syncModalTemplatePreviewState(template);
  6352. return;
  6353. }
  6354. await loadTemplatePreviewDetail(template);
  6355. };
  6356. const handleTemplateItemConfirm = async (template: TemplateListItem) => {
  6357. await handleTemplateItemClick(template);
  6358. await confirmTemplateSelect();
  6359. };
  6360. const closeTemplateModal = () => {
  6361. templateModalOpen.value = false;
  6362. templatePreviewRequestToken += 1;
  6363. templatePreviewLoading.value = false;
  6364. modalTemplateDetail.value = null;
  6365. modalTemplateItem.value = null;
  6366. };
  6367. const fetchAllTemplates = async () => {
  6368. // 模板选择弹窗没有分页交互,打开时直接单次拉取较大的 limit,避免多次连续请求。
  6369. const limit = 9999;
  6370. const resp = await tplPage({ page: 1, limit } as any);
  6371. const list = ((resp as any)?.list || (resp as any)?.records || []) as TemplateListItem[];
  6372. const result = Array.isArray(list) ? list : [];
  6373. const uniqueMap = new Map<string, TemplateListItem>();
  6374. result.forEach((item, index) => {
  6375. uniqueMap.set(String(item.id ?? `template_${index}`), item);
  6376. });
  6377. return Array.from(uniqueMap.values());
  6378. };
  6379. const loadTemplateList = async (options: { silent?: boolean; preferredId?: number } = {}) => {
  6380. templateListLoading.value = true;
  6381. try {
  6382. const list = await fetchAllTemplates();
  6383. templateList.value = mergeTemplateCollections(mockTemplateList, list);
  6384. templateListLoaded.value = templateList.value.length > 0;
  6385. if (!templateList.value.length && !options.silent) {
  6386. createMessage.info('模板列表暂无可用数据');
  6387. }
  6388. } catch (error) {
  6389. templateList.value = mergeTemplateCollections(mockTemplateList, templateList.value);
  6390. templateListLoaded.value = templateList.value.length > 0;
  6391. if (!options.silent && !templateList.value.length) {
  6392. createMessage.warning('模板列表接口暂不可用');
  6393. }
  6394. } finally {
  6395. templateListLoading.value = false;
  6396. }
  6397. syncActiveTemplateItem(options.preferredId ?? selectedTemplateId.value);
  6398. if (selectedTemplateId.value) {
  6399. syncSelectedTemplateState(selectedTemplateId.value);
  6400. }
  6401. };
  6402. const syncSelectedTemplateState = (templateId?: string | number | null) => {
  6403. const id = normalizeTemplateId(templateId ?? selectedTemplateId.value);
  6404. selectedTemplateId.value = id;
  6405. if (!id) {
  6406. templateDetail.value = null;
  6407. templateDetailFromMock.value = false;
  6408. activeTemplateItem.value = null;
  6409. return { id, matchedListItem: null, cachedDetail: null };
  6410. }
  6411. const matchedListItem = getTemplateListItemById(id);
  6412. const cachedDetail = templateDetailCache.get(id) || null;
  6413. templateDetail.value = cachedDetail
  6414. ? mergeTemplateInfo(matchedListItem, cachedDetail)
  6415. : matchedListItem || mockTemplateMap[id] || null;
  6416. templateDetailFromMock.value = !cachedDetail && !matchedListItem && !!mockTemplateMap[id];
  6417. if (templateDetail.value) {
  6418. mergeTemplateListItem(templateDetail.value);
  6419. }
  6420. activeTemplateItem.value =
  6421. getTemplateListItemById(id) || matchedListItem || (templateDetail.value as TemplateListItem | null) || null;
  6422. return { id, matchedListItem, cachedDetail };
  6423. };
  6424. const loadTemplateDetail = async (
  6425. templateId?: string | number | null,
  6426. options: { silent?: boolean; requestDetail?: boolean } = {},
  6427. ) => {
  6428. const id = normalizeTemplateId(templateId ?? selectedTemplateId.value);
  6429. if (!id) {
  6430. syncSelectedTemplateState(id);
  6431. return templateDetail.value;
  6432. }
  6433. if (!templateListLoaded.value && !templateListLoading.value) {
  6434. await loadTemplateList({ silent: true, preferredId: id });
  6435. }
  6436. const { matchedListItem, cachedDetail } = syncSelectedTemplateState(id);
  6437. if (!id || !options.requestDetail || cachedDetail) {
  6438. return templateDetail.value;
  6439. }
  6440. try {
  6441. const resp = await tplDetail({ id });
  6442. const detail = resp || null;
  6443. if (detail) {
  6444. templateDetailCache.set(id, detail);
  6445. }
  6446. templateDetail.value = detail
  6447. ? mergeTemplateInfo(matchedListItem, detail)
  6448. : matchedListItem || mockTemplateMap[id] || null;
  6449. templateDetailFromMock.value = !detail && !matchedListItem && !!mockTemplateMap[id];
  6450. if (templateDetail.value) {
  6451. mergeTemplateListItem(templateDetail.value);
  6452. activeTemplateItem.value = getTemplateListItemById(id) || (templateDetail.value as TemplateListItem);
  6453. }
  6454. } catch (error) {
  6455. templateDetail.value = matchedListItem || mockTemplateMap[id] || null;
  6456. templateDetailFromMock.value = !matchedListItem && !!mockTemplateMap[id];
  6457. activeTemplateItem.value = (matchedListItem || (templateDetail.value as TemplateListItem | null)) ?? null;
  6458. if (!options.silent) {
  6459. createMessage.warning('模板详情接口暂不可用,已使用当前列表匹配结果');
  6460. }
  6461. }
  6462. return templateDetail.value;
  6463. };
  6464. const openTemplateModal = async () => {
  6465. templateModalOpen.value = true;
  6466. activeTemplateTab.value = 'template';
  6467. activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
  6468. templateKeyword.value = '';
  6469. templateSource.value = 0;
  6470. await Promise.all([
  6471. loadTemplateCategories(),
  6472. loadTemplateList({ silent: true, preferredId: selectedTemplateId.value }),
  6473. ]);
  6474. const previewTarget = syncModalTemplateItem(selectedTemplateId.value);
  6475. await loadTemplatePreviewDetail(previewTarget, { preferAppliedDetail: true });
  6476. };
  6477. watch([activeCategory, templateSource, templateKeyword, activeTemplateTab], async () => {
  6478. if (!templateModalOpen.value) return;
  6479. if (!showTemplateTabSwitch.value) {
  6480. activeTemplateTab.value = 'template';
  6481. }
  6482. const previewTarget = syncModalTemplateItem(
  6483. activeTemplateTab.value === 'plugin' ? undefined : selectedTemplateId.value,
  6484. );
  6485. await loadTemplatePreviewDetail(previewTarget, {
  6486. preferAppliedDetail:
  6487. activeTemplateTab.value !== 'plugin' && normalizeTemplateId(previewTarget?.id) === selectedTemplateId.value,
  6488. });
  6489. });
  6490. watch(activeTemplateTab, (tab) => {
  6491. if (!templateModalOpen.value) return;
  6492. if (tab === 'plugin') {
  6493. modalTemplateDetail.value = null;
  6494. templatePreviewLoading.value = false;
  6495. templatePreviewRequestToken += 1;
  6496. }
  6497. });
  6498. watch(categories, (nextCategories) => {
  6499. if (!nextCategories.includes(activeCategory.value)) {
  6500. activeCategory.value = DEFAULT_TEMPLATE_CATEGORY;
  6501. }
  6502. });
  6503. const modalPreviewTemplate = computed(() => mergeTemplateInfo(modalTemplateItem.value, modalTemplateDetail.value));
  6504. const previewRules = computed(() => {
  6505. const fields = buildTargetFieldsFromTemplateInfo(modalPreviewTemplate.value);
  6506. if (fields.length) {
  6507. return fields.map((item) => ({
  6508. targetField: item.label || '-',
  6509. originalField: item.key || '-',
  6510. required: item.required,
  6511. }));
  6512. }
  6513. return getTemplateHeaderList(modalPreviewTemplate.value).map((item) => ({
  6514. targetField: item,
  6515. originalField: item,
  6516. required: false,
  6517. }));
  6518. });
  6519. const modalTemplateHeaderList = computed(() => getTemplateHeaderList(modalPreviewTemplate.value));
  6520. const modalTemplateSourceLabel = computed(() => getTemplateSourceLabel(modalPreviewTemplate.value));
  6521. const modalTemplateFieldCount = computed(() => previewRules.value.length);
  6522. const activeNodeInfo = computed(() => {
  6523. return activeNodeKey.value ? findTreeNodeByKey(treeData.value, activeNodeKey.value)?.raw || null : null;
  6524. });
  6525. const activeNodeTemplateMatchedFail = computed(() => isTemplateMatchedFailStatus(activeNodeInfo.value?.fileStatus));
  6526. watch(
  6527. [
  6528. selectedTemplateId,
  6529. templateDetail,
  6530. activeTemplateItem,
  6531. fieldMapping,
  6532. rightTableHeaders,
  6533. addedColumns,
  6534. selectRules,
  6535. selectedRules,
  6536. headerRowIndex,
  6537. addedColumnConfig,
  6538. splitColumnConfig,
  6539. recognitionDirectionFields,
  6540. recognitionBasicInfoBaseFuncRegx,
  6541. recognitionBasicInfoManualVisible,
  6542. recognitionBasicInfoRows,
  6543. recognitionAmountZfEnabled,
  6544. childTemplateName,
  6545. ],
  6546. () => {
  6547. syncActiveNodeStructure();
  6548. },
  6549. { deep: true },
  6550. );
  6551. const hasTemplateSelected = computed(() => {
  6552. const activeTemplateId = normalizeTemplateId(
  6553. templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  6554. );
  6555. if (activeTemplateId > 0) return true;
  6556. if (activeNodeTemplateMatchedFail.value) return false;
  6557. return (
  6558. normalizeTemplateId(activeNodeInfo.value?.templateId) > 0 ||
  6559. normalizeTableRuleList(activeNodeInfo.value?.tableRuleList).length > 0 ||
  6560. !!normalizeRuleText(activeNodeInfo.value?.tableName)
  6561. );
  6562. });
  6563. const activeSheetDisplayName = computed(() => {
  6564. return activeNodeInfo.value?.sheetName || activeNodeInfo.value?.fileName || '当前工作表';
  6565. });
  6566. const currentTemplateName = computed(() => {
  6567. return (
  6568. templateDetail.value?.tableNameCn ||
  6569. activeTemplateItem.value?.tableNameCn ||
  6570. activeNodeInfo.value?.tableName ||
  6571. '-'
  6572. );
  6573. });
  6574. const currentTemplateDisplayName = computed(() => {
  6575. if (!hasTemplateSelected.value) return '未选择模板';
  6576. return currentTemplateName.value === '-' ? '已选择模板' : currentTemplateName.value;
  6577. });
  6578. const showRecognitionAmountZfPanel = computed(
  6579. () => recognitionAmountZfEnabled.value || fixedTargetFields.value.some((item) => isTransAmountTargetField(item)),
  6580. );
  6581. const recognitionRuleTabs = computed(() => {
  6582. const tabs: string[] = [];
  6583. if (showRecognitionAmountZfPanel.value) {
  6584. tabs.push('正负标识');
  6585. }
  6586. if (hasRecognitionLoanTab.value) {
  6587. tabs.push('借贷标志');
  6588. }
  6589. if (hasRecognitionCommunicationTab.value) {
  6590. tabs.push('通信类别');
  6591. }
  6592. if (showRecognitionBasicInfoPanel.value) {
  6593. tabs.push('基本信息配置');
  6594. }
  6595. return Array.from(new Set(tabs));
  6596. });
  6597. const showRecognitionPanel = computed(() => recognitionRuleTabs.value.length > 0);
  6598. const activeRecognitionRuleDesc = computed(() => {
  6599. const templateName = currentTemplateName.value === '-' ? '当前模板' : currentTemplateName.value;
  6600. const ruleLabel = activeRecognitionRule.value || recognitionRuleTabs.value[0] || '识别规则';
  6601. return `围绕「${templateName}」预置 ${ruleLabel} 的识别配置,后续可结合导入样本继续沉淀成子模板。`;
  6602. });
  6603. watch(
  6604. recognitionRuleTabs,
  6605. (tabs) => {
  6606. if (!tabs.length) {
  6607. activeRecognitionRule.value = '';
  6608. return;
  6609. }
  6610. if (!tabs.includes(activeRecognitionRule.value)) {
  6611. activeRecognitionRule.value = tabs[0];
  6612. }
  6613. },
  6614. { immediate: true },
  6615. );
  6616. watch(
  6617. fixedTargetFields,
  6618. (fields) => {
  6619. if (activeMappingTargetKey.value && !fields.some((item) => item.key === activeMappingTargetKey.value)) {
  6620. activeMappingTargetKey.value = '';
  6621. }
  6622. },
  6623. { deep: true },
  6624. );
  6625. const confirmTemplateSelect = async () => {
  6626. if (!modalTemplateItem.value?.id) return;
  6627. if (Number(modalTemplateItem.value.id) < 0) {
  6628. createMessage.info('模板插件暂未接入详情逻辑');
  6629. closeTemplateModal();
  6630. return;
  6631. }
  6632. const selectedId = normalizeTemplateId(modalTemplateItem.value.id);
  6633. const previousSelectedId = normalizeTemplateId(selectedTemplateId.value);
  6634. const shouldResetTemplateState = !!previousSelectedId && previousSelectedId !== selectedId;
  6635. const sourcePreviewSnapshot = shouldResetTemplateState ? captureSourcePreviewSnapshot() : null;
  6636. if (shouldResetTemplateState) {
  6637. structureSyncSuspended.value = true;
  6638. try {
  6639. clearActiveNodeTemplateState();
  6640. resetNodeWorkState();
  6641. if (sourcePreviewSnapshot) {
  6642. restorePreviewSnapshot(sourcePreviewSnapshot);
  6643. } else {
  6644. await restorePreviewFromNode(activeNodeInfo.value);
  6645. }
  6646. } finally {
  6647. structureSyncSuspended.value = false;
  6648. }
  6649. }
  6650. selectedTemplateId.value = selectedId;
  6651. activeTemplateItem.value = modalTemplateItem.value;
  6652. if (modalTemplateDetail.value && normalizeTemplateId(modalTemplateDetail.value.id) === selectedId) {
  6653. templateDetailCache.set(selectedId, modalTemplateDetail.value);
  6654. }
  6655. closeTemplateModal();
  6656. await loadTemplateDetail(selectedTemplateId.value, { silent: true, requestDetail: true });
  6657. applyRecognitionDirectionState(activeNodeInfo.value, { preferTemplate: true });
  6658. applyRecognitionBasicInfoState(activeNodeInfo.value, { preferTemplate: true });
  6659. applyRecognitionAmountZfState(activeNodeInfo.value, { preferTemplate: true });
  6660. autoMatchFields(false);
  6661. saveCurrentNodeState();
  6662. };
  6663. const startResize = (type: 'left' | 'middle', event: MouseEvent) => {
  6664. event.preventDefault();
  6665. resizeState.value = {
  6666. type,
  6667. startX: event.clientX,
  6668. startLeft: leftPanelWidth.value,
  6669. startMiddle: middlePanelWidth.value,
  6670. };
  6671. document.body.classList.add('is-resizing-layout');
  6672. document.body.style.cursor = 'col-resize';
  6673. };
  6674. const applyResize = () => {
  6675. if (!resizeState.value.type || pendingClientX.value === null) return;
  6676. const deltaX = pendingClientX.value - resizeState.value.startX;
  6677. if (resizeState.value.type === 'left' && !leftCollapsed.value) {
  6678. const next = resizeState.value.startLeft + deltaX;
  6679. leftPanelWidth.value = Math.min(maxLeftWidth, Math.max(minLeftWidth, next));
  6680. }
  6681. if (resizeState.value.type === 'middle') {
  6682. const next = resizeState.value.startMiddle + deltaX;
  6683. middlePanelWidth.value = Math.min(maxMiddleWidth, Math.max(minMiddleWidth, next));
  6684. }
  6685. rafId.value = null;
  6686. };
  6687. const onResizeMouseMove = (event: MouseEvent) => {
  6688. if (!resizeState.value.type) return;
  6689. event.preventDefault();
  6690. pendingClientX.value = event.clientX;
  6691. if (rafId.value === null) {
  6692. rafId.value = window.requestAnimationFrame(applyResize);
  6693. }
  6694. };
  6695. const stopResize = () => {
  6696. resizeState.value.type = '';
  6697. resizeState.value.startX = 0;
  6698. pendingClientX.value = null;
  6699. if (rafId.value !== null) {
  6700. window.cancelAnimationFrame(rafId.value);
  6701. rafId.value = null;
  6702. }
  6703. document.body.classList.remove('is-resizing-layout');
  6704. document.body.style.cursor = '';
  6705. };
  6706. const resetAddColumnForm = () => {
  6707. addColumnDialogOpen.value = false;
  6708. addColumnForm.value = { name: '', mode: 'ref', contentFields: [], customContent: '' };
  6709. selectedSourceField.value = '';
  6710. selectedContentIndex.value = -1;
  6711. draggingContentIndex.value = null;
  6712. dragOverContentIndex.value = null;
  6713. };
  6714. const resetSplitForm = () => {
  6715. splitColumnDialogOpen.value = false;
  6716. splitColumnForm.value = {
  6717. source: '',
  6718. targetName1: '',
  6719. targetName2: '',
  6720. delimiterType: 'length',
  6721. delimiterValue: '',
  6722. };
  6723. };
  6724. const openAddColumn = () => {
  6725. addColumnMode.value = 'add';
  6726. addColumnDialogOpen.value = true;
  6727. addColumnForm.value = { name: '', mode: 'ref', contentFields: [], customContent: '' };
  6728. selectedSourceField.value = availableSourceHeaders.value[0] || '';
  6729. selectedContentIndex.value = -1;
  6730. draggingContentIndex.value = null;
  6731. dragOverContentIndex.value = null;
  6732. };
  6733. const openSplitColumn = () => {
  6734. if (!availableSourceHeaders.value.length) {
  6735. createMessage.warning('当前没有可供拆分的原始列');
  6736. return;
  6737. }
  6738. splitColumnDialogOpen.value = true;
  6739. splitColumnForm.value = {
  6740. source: availableSourceHeaders.value[0] || '',
  6741. targetName1: '',
  6742. targetName2: '',
  6743. delimiterType: 'length',
  6744. delimiterValue: '',
  6745. };
  6746. };
  6747. const openEditColumn = () => {
  6748. if (!selectedAddedColumn.value) {
  6749. createMessage.warning('请先选中一个新增列');
  6750. return;
  6751. }
  6752. addColumnMode.value = 'edit';
  6753. addColumnDialogOpen.value = true;
  6754. const config = addedColumnConfig.value[selectedAddedColumn.value];
  6755. addColumnForm.value = {
  6756. name: selectedAddedColumn.value,
  6757. mode: config?.mode || 'ref',
  6758. contentFields: (config?.contentFields || []).filter((field) => availableSourceHeaders.value.includes(field)),
  6759. customContent: config?.customContent || '',
  6760. };
  6761. selectedSourceField.value = availableSourceHeaders.value[0] || '';
  6762. selectedContentIndex.value = -1;
  6763. draggingContentIndex.value = null;
  6764. dragOverContentIndex.value = null;
  6765. };
  6766. const openDeleteColumn = () => {
  6767. if (!selectedAddedColumn.value) {
  6768. createMessage.warning('请先选中一个新增列');
  6769. return;
  6770. }
  6771. deleteColumnDialogOpen.value = true;
  6772. };
  6773. const appendSourceField = (field: string) => {
  6774. if (addColumnForm.value.mode !== 'ref') return;
  6775. if (!availableSourceHeaders.value.includes(field)) return;
  6776. addColumnForm.value.contentFields.push(field);
  6777. selectedContentIndex.value = addColumnForm.value.contentFields.length - 1;
  6778. };
  6779. const removeContentField = (index: number) => {
  6780. addColumnForm.value.contentFields.splice(index, 1);
  6781. if (selectedContentIndex.value >= addColumnForm.value.contentFields.length) {
  6782. selectedContentIndex.value = addColumnForm.value.contentFields.length - 1;
  6783. }
  6784. if (draggingContentIndex.value !== null && draggingContentIndex.value >= addColumnForm.value.contentFields.length) {
  6785. draggingContentIndex.value = null;
  6786. }
  6787. if (dragOverContentIndex.value !== null && dragOverContentIndex.value >= addColumnForm.value.contentFields.length) {
  6788. dragOverContentIndex.value = null;
  6789. }
  6790. };
  6791. const clearContentDragState = () => {
  6792. draggingContentIndex.value = null;
  6793. dragOverContentIndex.value = null;
  6794. };
  6795. const reorderContentFields = (fromIndex: number, insertIndex: number) => {
  6796. if (fromIndex < 0 || fromIndex >= addColumnForm.value.contentFields.length) return;
  6797. const list = [...addColumnForm.value.contentFields];
  6798. const [field] = list.splice(fromIndex, 1);
  6799. if (field === undefined) return;
  6800. const nextIndex = Math.min(Math.max(insertIndex - (fromIndex < insertIndex ? 1 : 0), 0), list.length);
  6801. list.splice(nextIndex, 0, field);
  6802. addColumnForm.value.contentFields = list;
  6803. selectedContentIndex.value = nextIndex;
  6804. draggingContentIndex.value = nextIndex;
  6805. dragOverContentIndex.value = nextIndex;
  6806. };
  6807. const resolveContentDropInsertIndex = (index: number, event: DragEvent) => {
  6808. const currentTarget = event.currentTarget as HTMLElement | null;
  6809. if (!currentTarget) return index;
  6810. const rect = currentTarget.getBoundingClientRect();
  6811. return event.clientY > rect.top + rect.height / 2 ? index + 1 : index;
  6812. };
  6813. const handleContentDragStart = (index: number, event: DragEvent) => {
  6814. draggingContentIndex.value = index;
  6815. dragOverContentIndex.value = index;
  6816. selectedContentIndex.value = index;
  6817. if (event.dataTransfer) {
  6818. event.dataTransfer.effectAllowed = 'move';
  6819. event.dataTransfer.setData('text/plain', String(index));
  6820. }
  6821. };
  6822. const handleContentDragEnter = (index: number) => {
  6823. if (draggingContentIndex.value === null) return;
  6824. dragOverContentIndex.value = index;
  6825. };
  6826. const handleContentDragOver = (index: number, event: DragEvent) => {
  6827. if (draggingContentIndex.value === null) return;
  6828. event.preventDefault();
  6829. dragOverContentIndex.value = index;
  6830. if (event.dataTransfer) {
  6831. event.dataTransfer.dropEffect = 'move';
  6832. }
  6833. };
  6834. const handleContentDrop = (index: number, event: DragEvent) => {
  6835. if (draggingContentIndex.value === null) return;
  6836. event.preventDefault();
  6837. const insertIndex = resolveContentDropInsertIndex(index, event);
  6838. reorderContentFields(draggingContentIndex.value, insertIndex);
  6839. clearContentDragState();
  6840. };
  6841. const handleContentDragEnd = () => {
  6842. clearContentDragState();
  6843. };
  6844. const getEnabledGroupingColumnsForHeader = (header: string) => {
  6845. if (!isCleaningRuleEnabledInUi('groupingColumns')) {
  6846. return [];
  6847. }
  6848. const columnIndex = rightTableHeaders.value.findIndex((item) => item === header);
  6849. if (columnIndex < 0) return [];
  6850. const groupingRule = [...(selectRules.value[columnIndex] || [])]
  6851. .reverse()
  6852. .find((rule) => rule.enabled && rule.key === 'groupingColumns');
  6853. if (!groupingRule) return [];
  6854. return normalizeGroupingColumnsDraft(groupingRule.rules?.groupColumns).filter(
  6855. (item) => !!item.header && !!item.direction && Number(item.count ?? 0) > 0,
  6856. );
  6857. };
  6858. const buildEnabledGroupingColumnsPayload = () =>
  6859. rightTableHeaders.value.reduce(
  6860. (acc, header) => {
  6861. if (!addedColumns.value.includes(header)) {
  6862. return acc;
  6863. }
  6864. const groupColumns = getEnabledGroupingColumnsForHeader(header);
  6865. if (!groupColumns.length) {
  6866. return acc;
  6867. }
  6868. acc.push({
  6869. targetHeader: header,
  6870. groupFlag: true,
  6871. groupColumns: cloneDeepSafe(groupColumns),
  6872. });
  6873. return acc;
  6874. },
  6875. [] as Array<{
  6876. targetHeader: string;
  6877. groupFlag: boolean;
  6878. groupColumns: Array<Record<string, any>>;
  6879. }>,
  6880. );
  6881. const setGeneratedColumnCell = (row: Record<string, string>, rowIndex: number, columnName: string, value: string) => {
  6882. row[columnName] = getGeneratedColumnHeaderRowIndex(columnName) === rowIndex ? columnName : value;
  6883. };
  6884. const rebuildGeneratedColumnsInSourceRows = () => {
  6885. if (!addedColumns.value.length || !sourceTableRows.value.length) return;
  6886. const addedHeaderSet = new Set(addedColumns.value);
  6887. sourceTableRows.value = sourceTableRows.value.map((row, rowIndex) => {
  6888. const nextRow = { ...row };
  6889. const handledSplitColumns = new Set<string>();
  6890. rightTableHeaders.value.forEach((header) => {
  6891. if (!addedHeaderSet.has(header)) return;
  6892. const groupingColumns = getEnabledGroupingColumnsForHeader(header);
  6893. if (groupingColumns.length) {
  6894. setGeneratedColumnCell(
  6895. nextRow,
  6896. rowIndex,
  6897. header,
  6898. String(groupColumnsFnValue(nextRow, groupingColumns) ?? ''),
  6899. );
  6900. return;
  6901. }
  6902. const splitConfig = splitColumnConfig.value[header];
  6903. if (splitConfig) {
  6904. const splitKey = [
  6905. splitConfig.source,
  6906. splitConfig.targetName1,
  6907. splitConfig.targetName2,
  6908. splitConfig.delimiterType,
  6909. splitConfig.delimiterValue,
  6910. ].join('::');
  6911. if (handledSplitColumns.has(splitKey)) return;
  6912. handledSplitColumns.add(splitKey);
  6913. const sourceValue = String(nextRow[splitConfig.source] ?? '');
  6914. const splitResult = splitSourceValueByConfig(
  6915. sourceValue,
  6916. splitConfig.delimiterType,
  6917. splitConfig.delimiterValue,
  6918. );
  6919. if (splitConfig.targetName1 && addedHeaderSet.has(splitConfig.targetName1)) {
  6920. setGeneratedColumnCell(nextRow, rowIndex, splitConfig.targetName1, splitResult.first);
  6921. }
  6922. if (splitConfig.targetName2 && addedHeaderSet.has(splitConfig.targetName2)) {
  6923. setGeneratedColumnCell(nextRow, rowIndex, splitConfig.targetName2, splitResult.second);
  6924. }
  6925. return;
  6926. }
  6927. const addedConfig = addedColumnConfig.value[header];
  6928. if (!addedConfig) return;
  6929. const nextValue =
  6930. addedConfig.mode === 'custom'
  6931. ? addedConfig.customContent || ''
  6932. : addedConfig.contentFields.map((field) => String(nextRow[field] ?? '')).join('');
  6933. setGeneratedColumnCell(nextRow, rowIndex, header, nextValue);
  6934. });
  6935. return nextRow;
  6936. });
  6937. };
  6938. const confirmAddOrEditColumn = () => {
  6939. const name = addColumnForm.value.name.trim();
  6940. const normalizedName = normalizeColumnNameForCompare(name);
  6941. const validContentFields = addColumnForm.value.contentFields.filter((field) =>
  6942. availableSourceHeaders.value.includes(field),
  6943. );
  6944. if (!name) {
  6945. createMessage.warning(addColumnMode.value === 'add' ? '请输入新增列名' : '请输入编辑列名');
  6946. return;
  6947. }
  6948. if (addColumnForm.value.mode === 'ref' && !validContentFields.length) {
  6949. createMessage.warning('请至少选择一个原始数据列');
  6950. return;
  6951. }
  6952. if (addColumnForm.value.mode === 'custom' && !addColumnForm.value.customContent.trim()) {
  6953. createMessage.warning('请输入自定义列内容');
  6954. return;
  6955. }
  6956. const saveConfig = {
  6957. mode: addColumnForm.value.mode,
  6958. contentFields: [...validContentFields],
  6959. customContent: addColumnForm.value.customContent,
  6960. };
  6961. if (addColumnMode.value === 'add') {
  6962. if (hasDuplicateHeaderName(name)) {
  6963. createMessage.warning('列名已存在');
  6964. return;
  6965. }
  6966. rightTableHeaders.value.push(name);
  6967. addedColumns.value.push(name);
  6968. addedColumnConfig.value[name] = saveConfig;
  6969. generatedColumnHeaderRowMap.value[name] = headerRowIndex.value;
  6970. sourceTableRows.value.forEach((row, rowIndex) => {
  6971. if (saveConfig.mode === 'custom') {
  6972. setGeneratedColumnCell(row, rowIndex, name, saveConfig.customContent || '');
  6973. } else {
  6974. setGeneratedColumnCell(
  6975. row,
  6976. rowIndex,
  6977. name,
  6978. saveConfig.contentFields.map((field) => row[field] || '').join(''),
  6979. );
  6980. }
  6981. });
  6982. selectedAddedColumn.value = name;
  6983. currentColumn.value = rightTableHeaders.value.findIndex((h) => h === name);
  6984. createMessage.success('新增列成功');
  6985. } else {
  6986. const oldName = selectedAddedColumn.value;
  6987. if (!oldName) return;
  6988. const preservedHeaderRowIndex = getGeneratedColumnHeaderRowIndex(oldName);
  6989. const normalizedOldName = normalizeColumnNameForCompare(oldName);
  6990. if (normalizedName !== normalizedOldName && hasDuplicateHeaderName(name, [oldName])) {
  6991. createMessage.warning('列名已存在');
  6992. return;
  6993. }
  6994. rightTableHeaders.value = rightTableHeaders.value.map((h) => (h === oldName ? name : h));
  6995. addedColumns.value = addedColumns.value.map((h) => (h === oldName ? name : h));
  6996. Object.keys(fieldMapping.value).forEach((key) => {
  6997. if (fieldMapping.value[key] === oldName) {
  6998. fieldMapping.value[key] = name;
  6999. }
  7000. });
  7001. const headerKeyMap = oldName && name !== oldName ? { [oldName]: name } : {};
  7002. if (Object.keys(headerKeyMap).length) {
  7003. selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, headerKeyMap);
  7004. groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, headerKeyMap);
  7005. }
  7006. if (name !== oldName) {
  7007. delete generatedColumnHeaderRowMap.value[oldName];
  7008. }
  7009. generatedColumnHeaderRowMap.value[name] = preservedHeaderRowIndex;
  7010. sourceTableRows.value.forEach((row, rowIndex) => {
  7011. if (saveConfig.mode === 'custom') {
  7012. setGeneratedColumnCell(row, rowIndex, name, saveConfig.customContent || '');
  7013. } else {
  7014. setGeneratedColumnCell(
  7015. row,
  7016. rowIndex,
  7017. name,
  7018. saveConfig.contentFields.map((field) => row[field] || '').join(''),
  7019. );
  7020. }
  7021. if (name !== oldName) {
  7022. delete row[oldName];
  7023. }
  7024. });
  7025. delete addedColumnConfig.value[oldName];
  7026. addedColumnConfig.value[name] = saveConfig;
  7027. selectedAddedColumn.value = name;
  7028. currentColumn.value = rightTableHeaders.value.findIndex((h) => h === name);
  7029. createMessage.success('编辑列成功');
  7030. }
  7031. runRulePipeline();
  7032. resetAddColumnForm();
  7033. };
  7034. const confirmSplitColumn = () => {
  7035. const source = splitColumnForm.value.source;
  7036. const target1 = splitColumnForm.value.targetName1.trim();
  7037. const target2 = splitColumnForm.value.targetName2.trim();
  7038. const normalizedTarget1 = normalizeColumnNameForCompare(target1);
  7039. const normalizedTarget2 = normalizeColumnNameForCompare(target2);
  7040. const delimiterType = normalizeSplitColumnMode(splitColumnForm.value.delimiterType);
  7041. const delimiterValue = normalizeSplitColumnValue(
  7042. splitColumnForm.value.delimiterType,
  7043. splitColumnForm.value.delimiterValue,
  7044. );
  7045. const splitLength = resolveSplitColumnLength(
  7046. splitColumnForm.value.delimiterType,
  7047. splitColumnForm.value.delimiterValue,
  7048. );
  7049. if (!source) {
  7050. createMessage.warning('请选择需要拆分的列');
  7051. return;
  7052. }
  7053. if (!availableSourceHeaders.value.includes(source)) {
  7054. createMessage.warning('拆分来源只能选择原始列');
  7055. return;
  7056. }
  7057. if (!target1) {
  7058. createMessage.warning('请输入拆分后列名1');
  7059. return;
  7060. }
  7061. if (!target2) {
  7062. createMessage.warning('请输入拆分后列名2');
  7063. return;
  7064. }
  7065. if (normalizedTarget1 === normalizedTarget2) {
  7066. createMessage.warning('拆分后两个列名不能相同');
  7067. return;
  7068. }
  7069. if (hasDuplicateHeaderName(target1)) {
  7070. createMessage.warning('拆分后列名1已存在');
  7071. return;
  7072. }
  7073. if (hasDuplicateHeaderName(target2)) {
  7074. createMessage.warning('拆分后列名2已存在');
  7075. return;
  7076. }
  7077. if (delimiterType === 'length' && !splitLength) {
  7078. createMessage.warning('请输入正确的字符长度');
  7079. return;
  7080. }
  7081. if (delimiterType === 'special' && !delimiterValue) {
  7082. createMessage.warning('请输入特殊标识');
  7083. return;
  7084. }
  7085. // 拆分后新列统一追加到最右侧,保持新增列区域集中展示。
  7086. rightTableHeaders.value.push(target1, target2);
  7087. addedColumns.value.push(target1, target2);
  7088. generatedColumnHeaderRowMap.value[target1] = headerRowIndex.value;
  7089. generatedColumnHeaderRowMap.value[target2] = headerRowIndex.value;
  7090. sourceTableRows.value.forEach((row, rowIndex) => {
  7091. const sourceVal = String(row[source] || '');
  7092. const splitResult = splitSourceValueByConfig(sourceVal, delimiterType, delimiterValue);
  7093. setGeneratedColumnCell(row, rowIndex, target1, splitResult.first);
  7094. setGeneratedColumnCell(row, rowIndex, target2, splitResult.second);
  7095. });
  7096. // 记录拆分配置
  7097. const config = {
  7098. source,
  7099. delimiterType,
  7100. delimiterValue,
  7101. targetName1: target1,
  7102. targetName2: target2,
  7103. };
  7104. splitColumnConfig.value[target1] = config;
  7105. splitColumnConfig.value[target2] = config;
  7106. selectedAddedColumn.value = target1;
  7107. currentColumn.value = rightTableHeaders.value.findIndex((h) => h === target1);
  7108. runRulePipeline();
  7109. createMessage.success('拆分列成功');
  7110. resetSplitForm();
  7111. };
  7112. const confirmDeleteColumn = () => {
  7113. const name = selectedAddedColumn.value;
  7114. if (!name) return;
  7115. rightTableHeaders.value = rightTableHeaders.value.filter((h) => h !== name);
  7116. addedColumns.value = addedColumns.value.filter((h) => h !== name);
  7117. delete addedColumnConfig.value[name];
  7118. const deletedSplitConfig = splitColumnConfig.value[name];
  7119. delete splitColumnConfig.value[name];
  7120. if (deletedSplitConfig) {
  7121. [deletedSplitConfig.targetName1, deletedSplitConfig.targetName2].filter(Boolean).forEach((targetName) => {
  7122. const targetConfig = splitColumnConfig.value[targetName];
  7123. if (targetName === name || !targetConfig) return;
  7124. splitColumnConfig.value[targetName] = {
  7125. ...targetConfig,
  7126. targetName1: targetConfig.targetName1 === name ? '' : targetConfig.targetName1,
  7127. targetName2: targetConfig.targetName2 === name ? '' : targetConfig.targetName2,
  7128. };
  7129. });
  7130. }
  7131. delete generatedColumnHeaderRowMap.value[name];
  7132. sourceTableRows.value.forEach((row) => {
  7133. delete row[name];
  7134. });
  7135. Object.keys(fieldMapping.value).forEach((key) => {
  7136. if (fieldMapping.value[key] === name) {
  7137. fieldMapping.value[key] = '';
  7138. }
  7139. });
  7140. selectRules.value = remapSelectRuleHeaderRefs(selectRules.value, {}, new Set([name]));
  7141. groupingColumnsDraft.value = remapGroupingColumnRules(groupingColumnsDraft.value, {}, new Set([name]));
  7142. selectedAddedColumn.value = addedColumns.value[0] || '';
  7143. currentColumn.value = selectedAddedColumn.value
  7144. ? rightTableHeaders.value.findIndex((h) => h === selectedAddedColumn.value)
  7145. : null;
  7146. selectedRowIndex.value = null;
  7147. runRulePipeline();
  7148. deleteColumnDialogOpen.value = false;
  7149. createMessage.success('删除列成功');
  7150. };
  7151. const onClickHeader = (header: string) => {
  7152. const index = rightTableHeaders.value.findIndex((h) => h === header);
  7153. if (index < 0) return;
  7154. closeMappingPopoverPreview();
  7155. focusPreviewColumn(header, index, { resetSelectedRow: true });
  7156. };
  7157. const onClickCell = (rowIndex: number, header: string, colIndex: number) => {
  7158. closeMappingPopoverPreview();
  7159. selectedRowIndex.value = rowIndex;
  7160. focusPreviewColumn(header, colIndex, { resetSelectedRow: false });
  7161. };
  7162. const runRulePipeline = () => {
  7163. rebuildGeneratedColumnsInSourceRows();
  7164. const previewRows = runCleaningPreviewPipeline({
  7165. rows: sourceTableRows.value,
  7166. headers: rightTableHeaders.value,
  7167. columnRules: selectRules.value.map((rules) =>
  7168. rules.filter((rule) => rule.enabled && isCleaningRuleEnabledInUi(rule.key)),
  7169. ),
  7170. ignoredRows: ignoredRowsSet.value,
  7171. headerRowIndex: headerRowIndex.value,
  7172. });
  7173. tablePreviewRows.value = normalizePreviewRowsByTargetFormat(previewRows);
  7174. };
  7175. const rulesListCheck = (index: number) => {
  7176. activeRules.value = index;
  7177. cleaningRulePanelExpanded.value = true;
  7178. currentRuleTitle.value = '';
  7179. cleaningRulesList.value = cleaningRulesList.value.map((item, i) => {
  7180. if (index === 0) {
  7181. return { ...item, show: i !== 0, active: false };
  7182. }
  7183. return { ...item, show: i === 0, active: false };
  7184. });
  7185. };
  7186. const rulesListClick = (key: CleaningRuleKey) => {
  7187. cleaningRulePanelExpanded.value = true;
  7188. cleaningRulesList.value = cleaningRulesList.value.map((item) => ({
  7189. ...item,
  7190. active: item.key === key,
  7191. }));
  7192. currentRuleTitle.value = cleaningRulesList.value.find((item) => item.key === key)?.name || '';
  7193. if (isSharedRuleMode.value) {
  7194. selectedRules.value = selectedRules.value.map((item) => ({ ...item, active: false }));
  7195. return;
  7196. }
  7197. if (currentColumn.value !== null) {
  7198. const list = selectRules.value[currentColumn.value] || [];
  7199. selectRules.value[currentColumn.value] = list.map((item) => ({ ...item, active: false }));
  7200. }
  7201. };
  7202. const rulesListRight = () => {
  7203. const activeRule = cleaningRulesList.value.find((item) => item.active);
  7204. if (!activeRule) {
  7205. createMessage.warning('请先选择一个清洗规则');
  7206. return;
  7207. }
  7208. if (isSharedRuleMode.value && activeRule.key === 'ignoreLine') {
  7209. selectedRules.value = [
  7210. {
  7211. name: activeRule.name,
  7212. key: activeRule.key,
  7213. active: true,
  7214. enabled: false,
  7215. rules: defaultRuleConfig(),
  7216. },
  7217. ];
  7218. omitLineInput.value = String(selectedRules.value[0].rules.omitLine || '');
  7219. currentRuleTitle.value = activeRule.name;
  7220. return;
  7221. }
  7222. if (currentColumn.value === null) {
  7223. createMessage.warning('请先选中一列');
  7224. return;
  7225. }
  7226. if (
  7227. activeRule.key === 'groupingColumns' &&
  7228. !addedColumns.value.includes(rightTableHeaders.value[currentColumn.value])
  7229. ) {
  7230. createMessage.warning('组合列只能配置在新增列上');
  7231. return;
  7232. }
  7233. if (!selectRules.value[currentColumn.value]) {
  7234. selectRules.value[currentColumn.value] = [];
  7235. }
  7236. selectRules.value[currentColumn.value].push({
  7237. name: activeRule.name,
  7238. key: activeRule.key,
  7239. active: false,
  7240. enabled: false,
  7241. rules: defaultRuleConfig(),
  7242. });
  7243. const lastIndex = selectRules.value[currentColumn.value].length - 1;
  7244. selectRulesClick(lastIndex);
  7245. runRulePipeline();
  7246. };
  7247. const dbRulesListClick = (key: CleaningRuleKey) => {
  7248. rulesListClick(key);
  7249. rulesListRight();
  7250. };
  7251. const hydrateRuleFormByActiveRule = (rule?: SelectedRuleItem) => {
  7252. if (!rule) return;
  7253. if (rule.key === 'ignoreLine') {
  7254. omitLineInput.value = String(rule.rules?.omitLine || '');
  7255. }
  7256. if (rule.key === 'dateConfig') {
  7257. dateConfigType.value = Number(rule.rules?.dateType ?? 1) || 1;
  7258. }
  7259. if (rule.key === 'extractData') {
  7260. extractDataBefore.value = String(rule.rules?.beforeModel || '');
  7261. extractDataAfter.value = String(rule.rules?.nextModel || '');
  7262. syncExtractDataMode();
  7263. }
  7264. if (rule.key === 'extractSegment') {
  7265. extractSegmentValue.value = String(rule.rules?.segment || '');
  7266. extractSegmentRow.value = Number(rule.rules?.segmentRow ?? 1) || 1;
  7267. }
  7268. if (rule.key === 'absoluteValue') {
  7269. absoluteValueChecked.value = toRuleBoolean(rule.rules?.absoluteData, false);
  7270. }
  7271. if (rule.key === 'decimalConversion') {
  7272. decimalEnabled.value = toRuleBoolean(rule.rules?.decimalData, false);
  7273. decimalSourceBase.value = normalizeDecimalBaseLabel(rule.rules?.decimalModel2 || '10进制') || '10进制';
  7274. decimalTargetBase.value = normalizeDecimalBaseLabel(rule.rules?.decimalModel || '16进制') || '16进制';
  7275. }
  7276. if (rule.key === 'replaceValue') {
  7277. replaceEnabled.value = toRuleBoolean(rule.rules?.replaceFlag, true);
  7278. replaceMode.value = Number(rule.rules?.replaceModel ?? 0) === 1 ? 1 : 0;
  7279. replaceSource.value = String(rule.rules?.sourceData || '');
  7280. replaceTarget.value = String(rule.rules?.targetData || '');
  7281. }
  7282. if (rule.key === 'removeSpecialCharacters') {
  7283. const specialList: string[] = rule.rules?.specialList || [];
  7284. removeSpaceChecked.value = specialList.includes('移除空格字符');
  7285. removeTabChecked.value = specialList.includes('移除换行字符(tab)');
  7286. removeCustomChecked.value = specialList.includes('要移除的特殊字符');
  7287. removeCustomText.value = String(rule.rules?.specialStr || '');
  7288. }
  7289. if (rule.key === 'groupingColumns') {
  7290. groupingEnabled.value = toRuleBoolean(rule.rules?.groupFlag, false);
  7291. groupingColumnsDraft.value = normalizeGroupingColumnsDraft(rule.rules?.groupColumns);
  7292. }
  7293. if (rule.key === 'magnificationAdjustment') {
  7294. magnificationValue.value = Number(rule.rules?.magnificationAdjustmentValue ?? 1) || 1;
  7295. }
  7296. };
  7297. const selectRulesClick = (index: number) => {
  7298. if (isSharedRuleMode.value) {
  7299. selectedRules.value = selectedRules.value.map((item, idx) => ({
  7300. ...item,
  7301. active: idx === index,
  7302. }));
  7303. const activeRule = selectedRules.value[index];
  7304. currentRuleTitle.value = activeRule?.name || '';
  7305. hydrateRuleFormByActiveRule(activeRule);
  7306. return;
  7307. }
  7308. if (currentColumn.value === null) return;
  7309. const list = selectRules.value[currentColumn.value] || [];
  7310. selectRules.value[currentColumn.value] = list.map((item, idx) => ({
  7311. ...item,
  7312. active: idx === index,
  7313. }));
  7314. const activeRule = selectRules.value[currentColumn.value][index];
  7315. currentRuleTitle.value = activeRule?.name || '';
  7316. hydrateRuleFormByActiveRule(activeRule);
  7317. };
  7318. const rulesListLeft = () => {
  7319. if (isSharedRuleMode.value) {
  7320. const activeIndex = selectedRules.value.findIndex((item) => item.active);
  7321. if (activeIndex < 0) {
  7322. createMessage.warning('请先选择一个已选规则');
  7323. return;
  7324. }
  7325. selectedRules.value.splice(activeIndex, 1);
  7326. selectedRules.value = [...selectedRules.value];
  7327. omitLineInput.value = '';
  7328. currentRuleTitle.value = '';
  7329. runRulePipeline();
  7330. return;
  7331. }
  7332. if (currentColumn.value === null) {
  7333. createMessage.warning('请先选中一列');
  7334. return;
  7335. }
  7336. const list = selectRules.value[currentColumn.value] || [];
  7337. const activeIndex = list.findIndex((item) => item.active);
  7338. if (activeIndex < 0) {
  7339. createMessage.warning('请先选择一个已选规则');
  7340. return;
  7341. }
  7342. list.splice(activeIndex, 1);
  7343. selectRules.value[currentColumn.value] = [...list];
  7344. currentRuleTitle.value = '';
  7345. runRulePipeline();
  7346. };
  7347. const selectRulesDel = () => {
  7348. if (isSharedRuleMode.value) {
  7349. selectedRules.value = [];
  7350. omitLineInput.value = '';
  7351. currentRuleTitle.value = '';
  7352. runRulePipeline();
  7353. return;
  7354. }
  7355. if (currentColumn.value === null) {
  7356. createMessage.warning('请先选中一列');
  7357. return;
  7358. }
  7359. selectRules.value[currentColumn.value] = [];
  7360. currentRuleTitle.value = '';
  7361. runRulePipeline();
  7362. };
  7363. const getCurrentActiveRule = (): SelectedRuleItem | undefined => {
  7364. if (isSharedRuleMode.value) {
  7365. return selectedRules.value.find((rule) => rule.active);
  7366. }
  7367. if (currentColumn.value === null) return undefined;
  7368. return (selectRules.value[currentColumn.value] || []).find((rule) => rule.active);
  7369. };
  7370. const refreshActiveRuleCollection = () => {
  7371. if (isSharedRuleMode.value) {
  7372. selectedRules.value = [...selectedRules.value];
  7373. return;
  7374. }
  7375. selectRules.value = [...selectRules.value];
  7376. };
  7377. const updateCurrentRuleConfig = (
  7378. expectedKey: CleaningRuleKey,
  7379. warningText: string,
  7380. successText: string,
  7381. updater: (rule: SelectedRuleItem) => void,
  7382. ) => {
  7383. const activeRule = getCurrentActiveRule();
  7384. if (!activeRule || activeRule.key !== expectedKey) {
  7385. createMessage.warning(warningText);
  7386. return false;
  7387. }
  7388. updater(activeRule);
  7389. activeRule.enabled = true;
  7390. refreshActiveRuleCollection();
  7391. runRulePipeline();
  7392. createMessage.success(successText);
  7393. return true;
  7394. };
  7395. const toggleCurrentRuleEnabled = (enabled: boolean) => {
  7396. const activeRule = getCurrentActiveRule();
  7397. if (!activeRule) {
  7398. createMessage.warning('请先选择一个已选规则');
  7399. return;
  7400. }
  7401. if (activeRule.enabled === enabled) {
  7402. return;
  7403. }
  7404. activeRule.enabled = enabled;
  7405. refreshActiveRuleCollection();
  7406. runRulePipeline();
  7407. createMessage.success(enabled ? '规则已启用' : '规则已停用');
  7408. };
  7409. const getFilledGroupingFieldCount = (item: GroupingColumnDraft) =>
  7410. [!!item.header, !!item.direction, item.count !== undefined && item.count > 0].filter(Boolean).length;
  7411. const addGroupingColumnDraft = () => {
  7412. groupingColumnsDraft.value = [...groupingColumnsDraft.value, createGroupingColumnDraft()];
  7413. };
  7414. const removeGroupingColumnDraft = (index: number) => {
  7415. const next = groupingColumnsDraft.value.filter((_, itemIndex) => itemIndex !== index);
  7416. groupingColumnsDraft.value = next.length ? next : normalizeGroupingColumnsDraft();
  7417. };
  7418. const handleGroupingColumnHeaderChange = (index: number, value: string) => {
  7419. const row = groupingColumnsDraft.value[index];
  7420. if (!row) return;
  7421. row.header = value;
  7422. row.title = value;
  7423. groupingColumnsDraft.value = [...groupingColumnsDraft.value];
  7424. };
  7425. const applyIgnoreLineRule = () => {
  7426. updateCurrentRuleConfig('ignoreLine', '请先选择忽略行规则', '忽略行规则已应用', (rule) => {
  7427. rule.rules.omitLine = omitLineInput.value || '';
  7428. });
  7429. };
  7430. const applyDateConfigRule = () => {
  7431. updateCurrentRuleConfig('dateConfig', '请先选择日期和时间规则', '日期和时间规则已应用', (rule) => {
  7432. rule.rules.dateType = dateConfigType.value;
  7433. });
  7434. };
  7435. const applyExtractDataRule = () => {
  7436. updateCurrentRuleConfig('extractData', '请先选择提取数据规则', '提取数据规则已应用', (rule) => {
  7437. rule.rules.beforeModel = extractDataMode.value === 'before' ? extractDataBefore.value || '' : '';
  7438. rule.rules.nextModel = extractDataMode.value === 'after' ? extractDataAfter.value || '' : '';
  7439. });
  7440. };
  7441. const applyExtractSegmentRule = () => {
  7442. if (!extractSegmentValue.value.trim()) {
  7443. createMessage.warning('请输入分隔符');
  7444. return;
  7445. }
  7446. updateCurrentRuleConfig('extractSegment', '请先选择分割提取规则', '分割提取规则已应用', (rule) => {
  7447. rule.rules.segment = extractSegmentValue.value || '';
  7448. rule.rules.segmentRow = Number(extractSegmentRow.value ?? 1) || 1;
  7449. });
  7450. };
  7451. const applyTimestampConversionRule = () => {
  7452. updateCurrentRuleConfig('timestampConversion', '请先选择时间戳转换规则', '时间戳转换规则已启用', (rule) => {
  7453. rule.rules.timeStamp = true;
  7454. });
  7455. };
  7456. const applyAbsoluteValueRule = () => {
  7457. updateCurrentRuleConfig('absoluteValue', '请先选择绝对值规则', '绝对值规则已应用', (rule) => {
  7458. rule.rules.absoluteData = absoluteValueChecked.value;
  7459. });
  7460. };
  7461. const applyDecimalConversionRule = () => {
  7462. updateCurrentRuleConfig('decimalConversion', '请先选择进制转换规则', '进制转换规则已应用', (rule) => {
  7463. rule.rules.decimalData = decimalEnabled.value;
  7464. rule.rules.decimalModel = decimalTargetBase.value;
  7465. rule.rules.decimalModel2 = decimalSourceBase.value;
  7466. });
  7467. };
  7468. const applyReplaceRule = () => {
  7469. updateCurrentRuleConfig('replaceValue', '请先选择条件替换规则', '条件替换规则已应用', (rule) => {
  7470. rule.rules.replaceFlag = replaceEnabled.value;
  7471. rule.rules.replaceModel = replaceMode.value;
  7472. rule.rules.sourceData = replaceSource.value || '';
  7473. rule.rules.targetData = replaceTarget.value || '';
  7474. });
  7475. };
  7476. const applyRemoveSpecialRule = () => {
  7477. const specialList: string[] = [];
  7478. if (removeSpaceChecked.value) specialList.push('移除空格字符');
  7479. if (removeTabChecked.value) specialList.push('移除换行字符(tab)');
  7480. if (removeCustomChecked.value) specialList.push('要移除的特殊字符');
  7481. updateCurrentRuleConfig('removeSpecialCharacters', '请先选择移除特殊字符规则', '移除特殊字符规则已应用', (rule) => {
  7482. rule.rules.specialList = specialList;
  7483. rule.rules.specialStr = removeCustomText.value || '';
  7484. });
  7485. };
  7486. const applyGroupingColumnsRule = () => {
  7487. const normalizedRows = groupingColumnsDraft.value.map((item) => createGroupingColumnDraft(item));
  7488. const hasInvalidRow = normalizedRows.some((item) => {
  7489. const filledCount = getFilledGroupingFieldCount(item);
  7490. return filledCount > 0 && filledCount < 3;
  7491. });
  7492. if (hasInvalidRow) {
  7493. createMessage.warning('组合列每一行都需要完整选择表头、方向和位数');
  7494. return;
  7495. }
  7496. const hasValidRow = normalizedRows.some((item) => getFilledGroupingFieldCount(item) === 3);
  7497. if (!hasValidRow) {
  7498. createMessage.warning('请至少配置一条完整的组合列规则');
  7499. return;
  7500. }
  7501. updateCurrentRuleConfig('groupingColumns', '请先选择组合列规则', '组合列规则已应用', (rule) => {
  7502. rule.rules.groupFlag = true;
  7503. rule.rules.groupColumns = normalizedRows;
  7504. });
  7505. };
  7506. const applyMagnificationRule = () => {
  7507. updateCurrentRuleConfig('magnificationAdjustment', '请先选择倍数放大器规则', '倍数放大器规则已应用', (rule) => {
  7508. rule.rules.magnificationAdjustmentValue = Number(magnificationValue.value ?? 1) || 1;
  7509. });
  7510. };
  7511. const applyYearCompleteRule = () => {
  7512. updateCurrentRuleConfig('yearComplete', '请先选择年份补全规则', '年份补全规则已启用', (rule) => {
  7513. rule.rules.yearCompleteEnabled = true;
  7514. });
  7515. };
  7516. const toggleLeft = () => {
  7517. leftCollapsed.value = !leftCollapsed.value;
  7518. };
  7519. const handleSelect = (keys: (string | number)[]) => {
  7520. const rawKey = keys[0] ? String(keys[0]) : '';
  7521. const nextKey = rawKey ? resolveSelectableTreeKey(rawKey) : '';
  7522. selectedKeys.value = nextKey ? [nextKey] : [];
  7523. if (nextKey) {
  7524. switchNodeState(nextKey);
  7525. }
  7526. };
  7527. const handleImportData = async () => {
  7528. saveCurrentNodeState();
  7529. syncActiveNodeStructure();
  7530. const sanitizedFileInfos = sanitizeFileInfoTree(fileList.value, { keepRuleKey: false });
  7531. if (!sanitizedFileInfos.length) {
  7532. createMessage.warning('未找到可导入的数据,请先上传并完成模板匹配');
  7533. return;
  7534. }
  7535. const matchedSheetsWithoutHeaderRow = collectMatchedSheetsWithoutHeaderRow(sanitizedFileInfos);
  7536. if (matchedSheetsWithoutHeaderRow.length) {
  7537. const previewNames = matchedSheetsWithoutHeaderRow.slice(0, 3).join('、');
  7538. const suffix =
  7539. matchedSheetsWithoutHeaderRow.length > 3 ? ` 等 ${matchedSheetsWithoutHeaderRow.length} 个工作表` : '';
  7540. createMessage.warning(`已匹配模板的工作表必须先填写表头所在行:${previewNames}${suffix}`);
  7541. return;
  7542. }
  7543. try {
  7544. const childTemplateNameValid = await ensureChildTemplateNamesAvailable(sanitizedFileInfos);
  7545. if (!childTemplateNameValid) {
  7546. return;
  7547. }
  7548. } catch (error) {
  7549. createMessage.error(getRequestErrorMessage(error, '模板名称校验失败'));
  7550. return;
  7551. }
  7552. sessionStorage.setItem(FILE_INFO_STORAGE_KEY, JSON.stringify(sanitizedFileInfos));
  7553. importingData.value = true;
  7554. try {
  7555. const resolveMappedHeaderByTargetField = (item?: TreeFileInfo | null, targetField?: string) => {
  7556. const normalizedTarget = normalizeFieldMatchText(targetField);
  7557. if (!normalizedTarget) return '';
  7558. const matchedRule = normalizeTableRuleList(item?.tableRuleList).find((rule, index) => {
  7559. const targetKey = normalizeFieldMatchText(getTableRuleTargetKey(rule, index));
  7560. const targetLabel = normalizeFieldMatchText(getTableRuleTargetLabel(rule, index));
  7561. return normalizedTarget === targetKey || normalizedTarget === targetLabel;
  7562. });
  7563. return normalizeRecognitionConfigText(matchedRule?.fileColName);
  7564. };
  7565. const buildImportRecognitionBasicInfoFuncRegx = (item?: TreeFileInfo | null) => {
  7566. const source = normalizeRecognitionConfigText(item?.funcRegx);
  7567. if (!source) return '';
  7568. const rows = parseRecognitionBasicInfoRows(source);
  7569. if (!rows.length) return source;
  7570. if (!hasRecognitionBasicInfoFilledContent(rows)) return '';
  7571. const payload = rows.reduce(
  7572. (acc, row) => {
  7573. const targetField = resolveRecognitionBasicInfoTargetField(row.targetField);
  7574. const headerMatch = normalizeRecognitionBasicInfoText(row.headerMatch);
  7575. const endFlag = normalizeRecognitionBasicInfoText(row.endFlag);
  7576. const mappedHeader = resolveMappedHeaderByTargetField(item, targetField);
  7577. const nextKey = headerMatch || mappedHeader || targetField;
  7578. if (!nextKey) return acc;
  7579. acc[nextKey] = [headerMatch, endFlag];
  7580. return acc;
  7581. },
  7582. {} as Record<string, string[]>,
  7583. );
  7584. return Object.keys(payload).length ? JSON.stringify(payload) : '';
  7585. };
  7586. const buildImportFileInfoPayload = (item?: TreeFileInfo | null): TreeFileInfo | null => {
  7587. if (!item) return null;
  7588. const nextItem = cloneDeepSafe(item) as TreeFileInfo;
  7589. const children = Array.isArray(nextItem.children)
  7590. ? nextItem.children
  7591. .map((child) => buildImportFileInfoPayload(child))
  7592. .filter((child): child is TreeFileInfo => Boolean(child))
  7593. : nextItem.children;
  7594. if (isFileRootItem(nextItem)) {
  7595. const rootPayload = {
  7596. ...nextItem,
  7597. children,
  7598. } as TreeFileInfo & { childTemplateName?: string | null };
  7599. delete rootPayload.childTemplateName;
  7600. return rootPayload;
  7601. }
  7602. const childName = normalizeRuleText(nextItem.childTemplateName);
  7603. const hasNew = !!childName || !!nextItem.hasNew;
  7604. const importTemplateId = normalizeTemplateId(
  7605. nextItem.templateId ?? templateDetail.value?.id ?? activeTemplateItem.value?.id ?? selectedTemplateId.value,
  7606. );
  7607. const currentTemplate = resolveTemplateInfoById(importTemplateId);
  7608. const parentTemplateId = normalizeTemplateId(nextItem.mainId) || normalizeTemplateId(currentTemplate?.mainId);
  7609. const parentTemplate = (parentTemplateId && resolveTemplateInfoById(parentTemplateId)) || currentTemplate;
  7610. const currentFuncRegx = normalizeRecognitionConfigText(nextItem.funcRegx);
  7611. const importFuncRegx = currentFuncRegx ? buildImportRecognitionBasicInfoFuncRegx(nextItem) : '';
  7612. const sheetPayload = {
  7613. ...nextItem,
  7614. tableName: hasNew ? childName : nextItem.tableName,
  7615. tableNameEn: hasNew
  7616. ? normalizeRuleText(parentTemplate?.tableNameEn) ||
  7617. normalizeRuleText(currentTemplate?.tableNameEn) ||
  7618. normalizeRuleText(nextItem.tableNameEn) ||
  7619. null
  7620. : nextItem.tableNameEn,
  7621. hasNew,
  7622. classify: hasNew
  7623. ? normalizeRuleText(parentTemplate?.classify) ||
  7624. normalizeRuleText(currentTemplate?.classify) ||
  7625. normalizeRuleText(nextItem.classify) ||
  7626. null
  7627. : nextItem.classify,
  7628. funcRegx: importFuncRegx || null,
  7629. children,
  7630. } as TreeFileInfo & { childTemplateName?: string | null };
  7631. return sheetPayload;
  7632. };
  7633. const importFileInfos = sanitizedFileInfos
  7634. .map((item) => buildImportFileInfoPayload(item))
  7635. .filter((item): item is TreeFileInfo => Boolean(item));
  7636. sessionStorage.setItem(IMPORT_FILE_INFO_STORAGE_KEY, JSON.stringify(importFileInfos));
  7637. resetImportProgressSession();
  7638. await go(PageEnum.BASE_CLEAN_PROGRESS);
  7639. } catch {
  7640. createMessage.error('导入数据失败');
  7641. } finally {
  7642. importingData.value = false;
  7643. }
  7644. };
  7645. const loadStoredFileList = () => {
  7646. const raw = sessionStorage.getItem(FILE_INFO_STORAGE_KEY);
  7647. if (!raw) return [];
  7648. try {
  7649. const parsed = JSON.parse(raw) as TreeFileInfo[];
  7650. return Array.isArray(parsed) ? parsed.map((item) => mergeStoredFileInfoWithMock(item)) : [];
  7651. } catch (error) {
  7652. return [];
  7653. }
  7654. };
  7655. const getFocusedNodeKeyFromQuery = () => {
  7656. const rawFocusKey = pageQuery.value?.focusKey;
  7657. return Array.isArray(rawFocusKey) ? normalizeRuleText(rawFocusKey[0]) : normalizeRuleText(rawFocusKey);
  7658. };
  7659. const getQueryText = (value?: string | string[] | null) =>
  7660. Array.isArray(value) ? normalizeRuleText(value[0]) : normalizeRuleText(value);
  7661. const handleBack = () => {
  7662. const from = getQueryText(pageQuery.value?.from);
  7663. if (from === 'match') {
  7664. const matchStatus = getQueryText(pageQuery.value?.matchStatus);
  7665. const focusKey = getFocusedNodeKeyFromQuery();
  7666. const matchQuery: Record<string, string> = {};
  7667. if (['all', 'success', 'fail'].includes(matchStatus)) {
  7668. matchQuery.status = matchStatus;
  7669. }
  7670. if (focusKey) {
  7671. matchQuery.focusKey = focusKey;
  7672. }
  7673. go({
  7674. path: PageEnum.BASE_MATCH,
  7675. query: Object.keys(matchQuery).length ? matchQuery : undefined,
  7676. });
  7677. return;
  7678. }
  7679. go(PageEnum.BASE_HOME);
  7680. };
  7681. const scrollSelectedTreeNodeIntoView = () => {
  7682. void nextTick(() => {
  7683. const selectedNode = document.querySelector(
  7684. '.case-data-cleaning-page .cleaning-file-tree .ant-tree-node-selected',
  7685. );
  7686. const target = (selectedNode?.closest('.ant-tree-treenode') || selectedNode) as HTMLElement | null;
  7687. target?.scrollIntoView({
  7688. block: 'center',
  7689. inline: 'nearest',
  7690. });
  7691. });
  7692. };
  7693. const loadFileTree = async () => {
  7694. const storedList = loadStoredFileList();
  7695. fileList.value = storedList.length ? storedList : sanitizeFileInfoTree(cloneDeepSafe(mockFileList));
  7696. if (!treeData.value.length) return;
  7697. const focusedKey = resolveSelectableTreeKey(getFocusedNodeKeyFromQuery());
  7698. const initialKey =
  7699. focusedKey ||
  7700. (selectedKeys.value.length
  7701. ? resolveSelectableTreeKey(String(selectedKeys.value[0]))
  7702. : findFirstSelectableKey(treeData.value));
  7703. if (!initialKey) return;
  7704. selectedKeys.value = [initialKey];
  7705. await switchNodeState(initialKey);
  7706. if (focusedKey) {
  7707. scrollSelectedTreeNodeIntoView();
  7708. }
  7709. };
  7710. const initializePage = async () => {
  7711. void Promise.all([loadTemplateCategories(), loadTemplateList({ silent: true })]);
  7712. runRulePipeline();
  7713. await loadFileTree();
  7714. };
  7715. onMounted(() => {
  7716. initializePage();
  7717. window.addEventListener('mousemove', onResizeMouseMove);
  7718. window.addEventListener('mouseup', stopResize);
  7719. });
  7720. onBeforeUnmount(() => {
  7721. saveCurrentNodeState();
  7722. window.removeEventListener('mousemove', onResizeMouseMove);
  7723. window.removeEventListener('mouseup', stopResize);
  7724. document.body.classList.remove('is-resizing-layout');
  7725. document.body.style.cursor = '';
  7726. });
  7727. </script>
  7728. <style lang="less">
  7729. @prefix-cls: ~'jeesite-case-data-cleaning';
  7730. .@{prefix-cls}.case-data-cleaning-page {
  7731. min-height: 0;
  7732. height: calc(100vh - 100px);
  7733. max-height: calc(100vh - 100px);
  7734. background: #fff;
  7735. padding: 12px 16px 24px;
  7736. box-sizing: border-box;
  7737. display: flex;
  7738. flex-direction: column;
  7739. gap: 12px;
  7740. overflow: hidden;
  7741. .page-header {
  7742. display: flex;
  7743. flex-direction: column;
  7744. gap: 4px;
  7745. padding: 0 2px;
  7746. }
  7747. .page-header-main {
  7748. display: flex;
  7749. align-items: flex-start;
  7750. justify-content: space-between;
  7751. gap: 16px;
  7752. }
  7753. .page-title-area {
  7754. display: flex;
  7755. align-items: flex-start;
  7756. gap: 10px;
  7757. min-width: 0;
  7758. flex: 1;
  7759. }
  7760. .page-title-block {
  7761. display: flex;
  7762. flex-direction: column;
  7763. gap: 4px;
  7764. min-width: 0;
  7765. }
  7766. .page-title {
  7767. font-size: 18px;
  7768. line-height: 1.4;
  7769. font-weight: 600;
  7770. color: #111827;
  7771. }
  7772. .page-subtitle {
  7773. font-size: 13px;
  7774. line-height: 1.6;
  7775. color: #6b7280;
  7776. }
  7777. .page-header-actions {
  7778. display: flex;
  7779. align-items: center;
  7780. justify-content: flex-end;
  7781. gap: 8px;
  7782. flex-wrap: wrap;
  7783. margin-left: auto;
  7784. :deep(.ant-btn) {
  7785. height: 32px;
  7786. border-radius: 10px;
  7787. }
  7788. }
  7789. .back-btn {
  7790. padding: 0;
  7791. min-width: 32px;
  7792. width: 32px;
  7793. height: 32px;
  7794. border-radius: 10px;
  7795. border: 1px solid #e5e7eb;
  7796. color: #475569;
  7797. background: #f8fafc;
  7798. flex-shrink: 0;
  7799. &:hover {
  7800. color: #2563eb;
  7801. border-color: #bfdbfe;
  7802. background: #eff6ff;
  7803. }
  7804. }
  7805. .main-layout {
  7806. flex: 1 1 auto;
  7807. min-height: 0;
  7808. display: grid;
  7809. gap: 0;
  7810. overflow-x: auto;
  7811. overflow-y: hidden;
  7812. }
  7813. .panel-resizer {
  7814. width: 6px;
  7815. cursor: col-resize;
  7816. background: transparent;
  7817. position: relative;
  7818. }
  7819. .left-panel,
  7820. .middle-panel,
  7821. .right-panel {
  7822. background: #fff;
  7823. border: 1px solid #e5e7eb;
  7824. border-radius: 14px;
  7825. min-height: 0;
  7826. overflow: hidden;
  7827. display: flex;
  7828. flex-direction: column;
  7829. box-shadow: 0 10px 28px rgba(15, 23, 42, 0.04);
  7830. }
  7831. .tree-header {
  7832. display: flex;
  7833. align-items: center;
  7834. justify-content: space-between;
  7835. padding: 14px 16px 12px;
  7836. border-bottom: 1px solid #eef2f7;
  7837. .label {
  7838. font-size: 15px;
  7839. font-weight: 600;
  7840. color: #111827;
  7841. }
  7842. .actions {
  7843. display: flex;
  7844. gap: 6px;
  7845. }
  7846. }
  7847. .tree-wrap {
  7848. flex: 1;
  7849. min-height: 0;
  7850. overflow: auto;
  7851. padding: 8px;
  7852. .cleaning-file-tree {
  7853. :deep(.ant-tree) {
  7854. margin: 0;
  7855. }
  7856. :deep(.ant-tree-treenode) {
  7857. padding: 2px 0;
  7858. }
  7859. :deep(.ant-tree-node-content-wrapper) {
  7860. min-width: 0;
  7861. border-radius: 10px;
  7862. }
  7863. :deep(.ant-tree-node-content-wrapper.ant-tree-node-selected) {
  7864. background: #eaf1ff;
  7865. }
  7866. :deep(.ant-tree-title) {
  7867. width: 100%;
  7868. min-width: 0;
  7869. }
  7870. .tree-file-row,
  7871. .tree-sheet-row {
  7872. width: 100%;
  7873. min-width: 0;
  7874. display: flex;
  7875. align-items: center;
  7876. box-sizing: border-box;
  7877. }
  7878. .tree-file-row {
  7879. padding: 2px 6px 2px 0;
  7880. }
  7881. .tree-file-name {
  7882. overflow: hidden;
  7883. text-overflow: ellipsis;
  7884. white-space: nowrap;
  7885. font-size: 14px;
  7886. font-weight: 600;
  7887. color: #1f2937;
  7888. }
  7889. .tree-sheet-row {
  7890. gap: 10px;
  7891. padding: 6px 8px 6px 4px;
  7892. border-radius: 8px;
  7893. }
  7894. .tree-sheet-status {
  7895. width: 18px;
  7896. height: 18px;
  7897. border-radius: 50%;
  7898. display: inline-flex;
  7899. align-items: center;
  7900. justify-content: center;
  7901. flex-shrink: 0;
  7902. font-size: 12px;
  7903. font-weight: 700;
  7904. color: #fff;
  7905. }
  7906. .tree-sheet-row.is-success {
  7907. .tree-sheet-status {
  7908. background: #22c55e;
  7909. }
  7910. }
  7911. .tree-sheet-row.is-fail {
  7912. .tree-sheet-status {
  7913. background: #ef4444;
  7914. }
  7915. }
  7916. .tree-sheet-row.is-analyzing {
  7917. .tree-sheet-status {
  7918. background: linear-gradient(180deg, #60a5fa 0%, #2563eb 100%);
  7919. }
  7920. }
  7921. .tree-sheet-status-spinner {
  7922. width: 10px;
  7923. height: 10px;
  7924. border-radius: 50%;
  7925. border: 2px solid rgba(255, 255, 255, 0.34);
  7926. border-top-color: #ffffff;
  7927. animation: cleaning-sheet-status-spin 0.8s linear infinite;
  7928. box-sizing: border-box;
  7929. }
  7930. .tree-sheet-name {
  7931. min-width: 0;
  7932. overflow: hidden;
  7933. text-overflow: ellipsis;
  7934. white-space: nowrap;
  7935. font-size: 13px;
  7936. color: #334155;
  7937. }
  7938. :deep(.ant-tree-node-content-wrapper.ant-tree-node-selected) {
  7939. .tree-sheet-name {
  7940. color: #2547d0;
  7941. font-weight: 600;
  7942. }
  7943. }
  7944. }
  7945. }
  7946. .legend {
  7947. border-top: 1px solid #eef2f7;
  7948. padding: 10px 12px;
  7949. background: linear-gradient(180deg, #fbfcff 0%, #f7f9fc 100%);
  7950. display: flex;
  7951. flex-direction: column;
  7952. align-items: stretch;
  7953. gap: 6px;
  7954. .legend-item {
  7955. display: flex;
  7956. align-items: center;
  7957. gap: 8px;
  7958. min-height: 20px;
  7959. font-size: 12px;
  7960. color: #475569;
  7961. line-height: 1.4;
  7962. }
  7963. .legend-icon {
  7964. width: 16px;
  7965. text-align: center;
  7966. flex-shrink: 0;
  7967. font-size: 15px;
  7968. font-weight: 700;
  7969. &.is-unmatched {
  7970. color: #cc3333;
  7971. }
  7972. &.is-matched {
  7973. color: #4ba64b;
  7974. }
  7975. &.is-formatError {
  7976. color: #f2a100;
  7977. }
  7978. }
  7979. .legend-dot {
  7980. width: 7px;
  7981. height: 7px;
  7982. border-radius: 50%;
  7983. display: inline-block;
  7984. background: currentColor;
  7985. }
  7986. .legend-text {
  7987. min-width: 0;
  7988. white-space: nowrap;
  7989. }
  7990. }
  7991. .panel-header {
  7992. display: flex;
  7993. align-items: flex-start;
  7994. justify-content: space-between;
  7995. gap: 12px;
  7996. padding: 18px 18px 10px;
  7997. }
  7998. .panel-title {
  7999. font-size: 15px;
  8000. font-weight: 600;
  8001. color: #111827;
  8002. }
  8003. .panel-desc {
  8004. margin-top: 4px;
  8005. font-size: 12px;
  8006. line-height: 1.6;
  8007. color: #6b7280;
  8008. }
  8009. .right-panel-body {
  8010. flex: 1 1 auto;
  8011. min-height: 0;
  8012. display: grid;
  8013. grid-template-rows: minmax(0, 1fr) auto;
  8014. gap: 12px;
  8015. padding: 12px 16px 16px;
  8016. overflow: hidden;
  8017. }
  8018. .template-match-bar {
  8019. display: flex;
  8020. align-items: center;
  8021. justify-content: space-between;
  8022. gap: 12px;
  8023. padding: 18px 18px 10px;
  8024. border-bottom: 1px solid #eef2f7;
  8025. }
  8026. .template-match-main {
  8027. display: flex;
  8028. align-items: center;
  8029. gap: 12px;
  8030. min-width: 0;
  8031. }
  8032. .template-match-icon {
  8033. width: 28px;
  8034. height: 28px;
  8035. border-radius: 8px;
  8036. background: linear-gradient(135deg, #46b96f 0%, #269656 100%);
  8037. position: relative;
  8038. flex-shrink: 0;
  8039. box-shadow: 0 8px 16px rgba(38, 150, 86, 0.18);
  8040. &::before,
  8041. &::after {
  8042. content: '';
  8043. position: absolute;
  8044. background: rgba(255, 255, 255, 0.92);
  8045. }
  8046. &::before {
  8047. width: 14px;
  8048. height: 2px;
  8049. left: 7px;
  8050. top: 8px;
  8051. box-shadow: 0 5px 0 rgba(255, 255, 255, 0.92);
  8052. }
  8053. &::after {
  8054. width: 2px;
  8055. height: 14px;
  8056. left: 13px;
  8057. top: 7px;
  8058. box-shadow:
  8059. -5px 0 0 rgba(255, 255, 255, 0.92),
  8060. 5px 0 0 rgba(255, 255, 255, 0.92);
  8061. }
  8062. }
  8063. .template-match-copy {
  8064. min-width: 0;
  8065. display: flex;
  8066. align-items: baseline;
  8067. gap: 8px;
  8068. flex-wrap: wrap;
  8069. }
  8070. .template-match-label {
  8071. font-size: 13px;
  8072. font-weight: 600;
  8073. color: #374151;
  8074. white-space: nowrap;
  8075. }
  8076. .template-match-name {
  8077. display: inline-flex;
  8078. align-items: center;
  8079. gap: 8px;
  8080. min-width: 0;
  8081. color: #2643b5;
  8082. font-size: 15px;
  8083. font-weight: 600;
  8084. line-height: 1.4;
  8085. }
  8086. .template-match-badge {
  8087. padding: 2px 8px;
  8088. border-radius: 999px;
  8089. background: #ecf3ff;
  8090. color: #4b63d3;
  8091. font-size: 12px;
  8092. font-weight: 500;
  8093. }
  8094. .template-select-trigger {
  8095. padding: 0;
  8096. font-size: 14px;
  8097. font-weight: 500;
  8098. flex-shrink: 0;
  8099. }
  8100. .template-match-bar.is-empty {
  8101. border-bottom-color: #e8eefb;
  8102. background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
  8103. }
  8104. .template-match-badge.is-pending {
  8105. background: #eef4ff;
  8106. color: #3557d6;
  8107. box-shadow: inset 0 0 0 1px #d9e5ff;
  8108. }
  8109. .middle-panel-body {
  8110. flex: 1;
  8111. min-height: 0;
  8112. display: flex;
  8113. flex-direction: column;
  8114. gap: 12px;
  8115. padding: 12px 16px 16px;
  8116. }
  8117. .middle-empty-shell {
  8118. flex: 1;
  8119. min-height: 0;
  8120. display: flex;
  8121. flex-direction: column;
  8122. gap: 14px;
  8123. padding: 16px;
  8124. background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
  8125. }
  8126. .middle-empty-stage {
  8127. flex: 1;
  8128. min-height: 360px;
  8129. display: flex;
  8130. flex-direction: column;
  8131. border: 1px solid #e4ebf5;
  8132. border-radius: 18px;
  8133. padding: 16px 18px 18px;
  8134. background:
  8135. radial-gradient(circle at top right, rgba(59, 91, 219, 0.08) 0%, rgba(59, 91, 219, 0) 34%),
  8136. linear-gradient(180deg, #fcfdff 0%, #f8fbff 100%);
  8137. box-shadow:
  8138. inset 0 1px 0 rgba(255, 255, 255, 0.94),
  8139. 0 12px 30px rgba(148, 163, 184, 0.08);
  8140. }
  8141. .middle-empty-stage-head {
  8142. display: flex;
  8143. align-items: center;
  8144. justify-content: space-between;
  8145. gap: 12px;
  8146. flex-wrap: wrap;
  8147. }
  8148. .middle-empty-badge {
  8149. display: inline-flex;
  8150. align-items: center;
  8151. height: 28px;
  8152. padding: 0 12px;
  8153. border-radius: 999px;
  8154. background: rgba(47, 79, 210, 0.08);
  8155. color: #2f4fd2;
  8156. font-size: 12px;
  8157. font-weight: 600;
  8158. letter-spacing: 0.02em;
  8159. }
  8160. .middle-empty-sheet {
  8161. color: #64748b;
  8162. font-size: 12px;
  8163. line-height: 1.6;
  8164. }
  8165. .middle-empty-stage-main {
  8166. flex: 1;
  8167. min-height: 0;
  8168. display: flex;
  8169. flex-direction: column;
  8170. align-items: center;
  8171. justify-content: center;
  8172. gap: 14px;
  8173. text-align: center;
  8174. }
  8175. .middle-empty-visual {
  8176. position: relative;
  8177. width: 148px;
  8178. height: 108px;
  8179. pointer-events: none;
  8180. &__halo,
  8181. &__card,
  8182. &__dot {
  8183. position: absolute;
  8184. }
  8185. &__halo {
  8186. left: 50%;
  8187. transform: translateX(-50%);
  8188. border-radius: 999px;
  8189. border: 1px solid rgba(137, 160, 255, 0.38);
  8190. background: linear-gradient(180deg, rgba(255, 255, 255, 0.92) 0%, rgba(233, 241, 255, 0.44) 100%);
  8191. &.is-large {
  8192. bottom: 6px;
  8193. width: 116px;
  8194. height: 34px;
  8195. box-shadow: 0 10px 20px rgba(59, 91, 219, 0.12);
  8196. }
  8197. &.is-small {
  8198. bottom: 24px;
  8199. width: 74px;
  8200. height: 74px;
  8201. background: radial-gradient(circle, rgba(255, 255, 255, 0.98) 0%, rgba(229, 237, 255, 0.52) 72%);
  8202. }
  8203. }
  8204. &__card {
  8205. left: 50%;
  8206. top: 18px;
  8207. width: 52px;
  8208. height: 64px;
  8209. border-radius: 16px;
  8210. border: 1px solid #d5e1ff;
  8211. background: linear-gradient(180deg, #ffffff 0%, #eef4ff 100%);
  8212. box-shadow: 0 12px 24px rgba(59, 91, 219, 0.14);
  8213. transform: translateX(-50%) rotate(-6deg);
  8214. &::before {
  8215. content: '';
  8216. position: absolute;
  8217. left: 10px;
  8218. top: 14px;
  8219. width: 22px;
  8220. height: 3px;
  8221. border-radius: 999px;
  8222. background: #b5c7ff;
  8223. box-shadow: 0 10px 0 #d0dcff;
  8224. }
  8225. &.is-back {
  8226. transform: translateX(-50%) translateX(18px) translateY(6px) rotate(8deg);
  8227. opacity: 0.72;
  8228. box-shadow: 0 8px 18px rgba(148, 163, 184, 0.12);
  8229. }
  8230. }
  8231. &__dot {
  8232. left: 50%;
  8233. bottom: 48px;
  8234. width: 14px;
  8235. height: 14px;
  8236. border-radius: 50%;
  8237. background: linear-gradient(180deg, #3b5bdb 0%, #6d8cff 100%);
  8238. box-shadow: 0 0 0 6px rgba(59, 91, 219, 0.08);
  8239. transform: translateX(-50%);
  8240. }
  8241. }
  8242. .middle-empty-actions {
  8243. display: flex;
  8244. flex-direction: column;
  8245. align-items: center;
  8246. gap: 10px;
  8247. }
  8248. .middle-empty-hint {
  8249. max-width: 340px;
  8250. color: #64748b;
  8251. font-size: 12px;
  8252. line-height: 1.7;
  8253. }
  8254. .middle-empty-highlights {
  8255. display: grid;
  8256. grid-template-columns: repeat(3, minmax(0, 1fr));
  8257. gap: 12px;
  8258. }
  8259. .middle-empty-card {
  8260. min-width: 0;
  8261. padding: 14px;
  8262. border: 1px solid #e6ebf3;
  8263. border-radius: 14px;
  8264. background: #fff;
  8265. box-shadow: 0 8px 22px rgba(148, 163, 184, 0.08);
  8266. &__index {
  8267. display: inline-flex;
  8268. align-items: center;
  8269. justify-content: center;
  8270. width: 30px;
  8271. height: 30px;
  8272. border-radius: 10px;
  8273. background: #eef4ff;
  8274. color: #3557d6;
  8275. font-size: 12px;
  8276. font-weight: 700;
  8277. }
  8278. &__title {
  8279. margin-top: 12px;
  8280. color: #1f2937;
  8281. font-size: 14px;
  8282. font-weight: 600;
  8283. line-height: 1.5;
  8284. }
  8285. &__desc {
  8286. margin-top: 6px;
  8287. color: #64748b;
  8288. font-size: 12px;
  8289. line-height: 1.7;
  8290. }
  8291. }
  8292. .mapping-table-card {
  8293. flex: 1;
  8294. min-height: 0;
  8295. display: flex;
  8296. flex-direction: column;
  8297. border: 1px solid #e5eaf2;
  8298. border-radius: 14px;
  8299. overflow: hidden;
  8300. background: #fff;
  8301. box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
  8302. }
  8303. .mapping-card-head {
  8304. padding: 12px 14px;
  8305. border-bottom: 1px solid #edf1f7;
  8306. background: linear-gradient(180deg, #fbfcff 0%, #f6f8fc 100%);
  8307. display: flex;
  8308. align-items: center;
  8309. justify-content: space-between;
  8310. gap: 12px;
  8311. }
  8312. .mapping-card-title {
  8313. font-size: 13px;
  8314. font-weight: 600;
  8315. color: #1f2937;
  8316. }
  8317. .mapping-card-actions {
  8318. display: flex;
  8319. align-items: center;
  8320. gap: 8px;
  8321. flex-wrap: wrap;
  8322. }
  8323. .toolbar,
  8324. .mapping-actions {
  8325. margin: 0 16px 0;
  8326. padding: 10px 14px;
  8327. display: flex;
  8328. gap: 8px;
  8329. border: 1px solid #e8edf5;
  8330. border-radius: 10px;
  8331. background: #f8fafc;
  8332. align-items: center;
  8333. flex-wrap: wrap;
  8334. box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
  8335. :deep(.ant-btn) {
  8336. height: 30px;
  8337. padding: 0 12px;
  8338. border-radius: 8px;
  8339. }
  8340. .toolbar-label {
  8341. margin-right: 2px;
  8342. color: #64748b;
  8343. font-size: 12px;
  8344. font-weight: 600;
  8345. white-space: nowrap;
  8346. }
  8347. }
  8348. .toolbar {
  8349. .current-template {
  8350. color: #475569;
  8351. font-size: 12px;
  8352. }
  8353. &--column-actions {
  8354. justify-content: space-between;
  8355. gap: 12px;
  8356. .toolbar-head {
  8357. display: flex;
  8358. align-items: center;
  8359. flex-shrink: 0;
  8360. }
  8361. .toolbar-label {
  8362. margin-right: 0;
  8363. padding-right: 12px;
  8364. position: relative;
  8365. }
  8366. .toolbar-label::after {
  8367. content: '';
  8368. position: absolute;
  8369. right: 0;
  8370. top: 50%;
  8371. width: 1px;
  8372. height: 14px;
  8373. background: #dbe2ea;
  8374. transform: translateY(-50%);
  8375. }
  8376. .toolbar-actions {
  8377. display: flex;
  8378. align-items: center;
  8379. justify-content: flex-start;
  8380. gap: 10px;
  8381. flex: 1;
  8382. flex-wrap: wrap;
  8383. min-width: 0;
  8384. :deep(.ant-btn) {
  8385. min-width: 74px;
  8386. border-color: #c8d7fb;
  8387. background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
  8388. color: #3651d9;
  8389. font-weight: 600;
  8390. box-shadow: none;
  8391. transition: all 0.2s ease;
  8392. }
  8393. :deep(.ant-btn:hover),
  8394. :deep(.ant-btn:focus) {
  8395. border-color: #9fb7fb;
  8396. background: #eef4ff;
  8397. color: #2744c7;
  8398. }
  8399. :deep(.ant-btn:active) {
  8400. border-color: #7f9cf5;
  8401. background: #e2ebff;
  8402. color: #1f3bb4;
  8403. }
  8404. :deep(.ant-btn-primary) {
  8405. border-color: #c8d7fb;
  8406. background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
  8407. color: #3651d9;
  8408. }
  8409. :deep(.ant-btn-primary:hover),
  8410. :deep(.ant-btn-primary:focus),
  8411. :deep(.ant-btn-primary:active) {
  8412. color: #2744c7;
  8413. }
  8414. }
  8415. .toolbar-meta {
  8416. display: inline-flex;
  8417. align-items: center;
  8418. gap: 10px;
  8419. min-height: 34px;
  8420. padding: 0 12px;
  8421. border: 1px dashed #d7e2f3;
  8422. border-radius: 10px;
  8423. background: #fff;
  8424. flex-shrink: 0;
  8425. }
  8426. .meta-label {
  8427. color: #64748b;
  8428. font-size: 12px;
  8429. font-weight: 600;
  8430. line-height: 1;
  8431. white-space: nowrap;
  8432. }
  8433. .meta-input {
  8434. width: 88px;
  8435. :deep(.ant-input) {
  8436. height: 28px;
  8437. padding: 0 10px;
  8438. border-radius: 8px;
  8439. text-align: center;
  8440. font-size: 12px;
  8441. }
  8442. }
  8443. :deep(.ant-btn) {
  8444. min-width: 76px;
  8445. height: 32px;
  8446. padding: 0 14px;
  8447. }
  8448. }
  8449. }
  8450. .mapping-actions {
  8451. margin-top: 12px;
  8452. .mapping-stat {
  8453. margin-left: 4px;
  8454. font-size: 12px;
  8455. color: #334155;
  8456. }
  8457. }
  8458. .mapping-table {
  8459. flex: 1;
  8460. min-height: 0;
  8461. overflow: auto;
  8462. .mapping-head,
  8463. .mapping-row {
  8464. display: grid;
  8465. grid-template-columns: 64px 72px minmax(150px, 1fr) minmax(168px, 220px);
  8466. align-items: center;
  8467. }
  8468. .mapping-head {
  8469. position: sticky;
  8470. top: 0;
  8471. z-index: 1;
  8472. background: #f4f7fb;
  8473. color: #334155;
  8474. font-weight: 600;
  8475. }
  8476. .mapping-row {
  8477. border-top: 1px solid #f1f5f9;
  8478. color: #475569;
  8479. cursor: pointer;
  8480. transition:
  8481. background-color 0.2s ease,
  8482. box-shadow 0.2s ease;
  8483. &:hover {
  8484. background: #fbfdff;
  8485. }
  8486. &.is-active {
  8487. background: #eef4ff;
  8488. box-shadow:
  8489. inset 3px 0 0 #3651d9,
  8490. inset 0 0 0 1px rgba(54, 81, 217, 0.18);
  8491. > span {
  8492. color: #243b7a;
  8493. }
  8494. .mapping-field-trigger {
  8495. border-color: #3651d9;
  8496. box-shadow: 0 0 0 3px rgba(59, 91, 219, 0.08);
  8497. }
  8498. }
  8499. }
  8500. .mapping-head > span,
  8501. .mapping-row > span {
  8502. padding: 8px 12px;
  8503. min-height: 50px;
  8504. font-size: 14px;
  8505. display: flex;
  8506. align-items: center;
  8507. box-sizing: border-box;
  8508. border-right: 1px solid #edf2f7;
  8509. }
  8510. .mapping-head > span:last-child,
  8511. .mapping-row > span:last-child {
  8512. border-right: 0;
  8513. }
  8514. .mapping-head > span:nth-child(1),
  8515. .mapping-head > span:nth-child(2),
  8516. .mapping-row > span:nth-child(1) {
  8517. justify-content: center;
  8518. }
  8519. .mapping-row > span:nth-child(2) {
  8520. justify-content: center;
  8521. }
  8522. .status-cell {
  8523. justify-content: center;
  8524. }
  8525. .mapping-status-group {
  8526. display: inline-flex;
  8527. align-items: center;
  8528. justify-content: center;
  8529. gap: 6px;
  8530. }
  8531. .mapping-status-badge {
  8532. width: 14px;
  8533. height: 14px;
  8534. display: inline-flex;
  8535. align-items: center;
  8536. justify-content: center;
  8537. &.is-matched {
  8538. color: #4ba64b;
  8539. }
  8540. &.is-required {
  8541. color: #cc3333;
  8542. }
  8543. &.is-optional {
  8544. color: transparent;
  8545. }
  8546. }
  8547. .mapping-status-dot {
  8548. width: 7px;
  8549. height: 7px;
  8550. border-radius: 50%;
  8551. display: inline-block;
  8552. background: currentColor;
  8553. }
  8554. .mapping-status-error {
  8555. width: 14px;
  8556. height: 14px;
  8557. border-radius: 50%;
  8558. display: inline-flex;
  8559. align-items: center;
  8560. justify-content: center;
  8561. flex: 0 0 14px;
  8562. background: #f59e0b;
  8563. color: #fff;
  8564. font-size: 10px;
  8565. font-weight: 700;
  8566. box-shadow: 0 4px 10px rgba(245, 158, 11, 0.18);
  8567. }
  8568. .target-field-cell {
  8569. color: #24324b;
  8570. }
  8571. .origin-field-cell {
  8572. padding: 8px 10px;
  8573. }
  8574. .required {
  8575. color: #ef4444;
  8576. margin-right: 4px;
  8577. }
  8578. .mapping-field-trigger {
  8579. width: 100%;
  8580. min-height: 34px;
  8581. padding: 0 12px;
  8582. border: 1px solid #d7dfeb;
  8583. border-radius: 8px;
  8584. background: #fff;
  8585. display: flex;
  8586. align-items: center;
  8587. justify-content: space-between;
  8588. gap: 10px;
  8589. cursor: pointer;
  8590. transition:
  8591. border-color 0.2s ease,
  8592. box-shadow 0.2s ease;
  8593. &:hover {
  8594. border-color: #b8c5da;
  8595. box-shadow: 0 0 0 3px rgba(59, 91, 219, 0.06);
  8596. }
  8597. &.is-disabled {
  8598. cursor: not-allowed;
  8599. border-color: #e2e8f0;
  8600. background: #f8fafc;
  8601. box-shadow: none;
  8602. &:hover {
  8603. border-color: #e2e8f0;
  8604. box-shadow: none;
  8605. }
  8606. }
  8607. }
  8608. .mapping-field-value {
  8609. flex: 1;
  8610. min-width: 0;
  8611. overflow: hidden;
  8612. text-overflow: ellipsis;
  8613. white-space: nowrap;
  8614. color: #334155;
  8615. font-size: 13px;
  8616. &.placeholder {
  8617. color: #9aa4b2;
  8618. }
  8619. }
  8620. .mapping-field-arrow {
  8621. flex-shrink: 0;
  8622. color: #64748b;
  8623. font-size: 12px;
  8624. }
  8625. .mapping-empty {
  8626. padding: 28px 16px;
  8627. text-align: center;
  8628. color: #94a3b8;
  8629. font-size: 13px;
  8630. }
  8631. }
  8632. .template-foot-panel {
  8633. display: flex;
  8634. flex-direction: column;
  8635. flex: 0 0 clamp(344px, 56%, 520px);
  8636. min-height: clamp(344px, 56%, 520px);
  8637. max-height: clamp(344px, 56%, 520px);
  8638. border: 1px solid #e6ebf3;
  8639. border-radius: 14px;
  8640. background: #fff;
  8641. overflow: hidden;
  8642. transition:
  8643. min-height 0.22s ease,
  8644. max-height 0.22s ease,
  8645. flex-basis 0.22s ease;
  8646. &.is-collapsed {
  8647. flex-basis: 156px;
  8648. min-height: 156px;
  8649. max-height: 156px;
  8650. .recognition-panel {
  8651. flex: 0 0 auto;
  8652. padding-bottom: 8px;
  8653. }
  8654. }
  8655. &.is-compact {
  8656. flex-basis: 108px;
  8657. min-height: 108px;
  8658. max-height: 108px;
  8659. }
  8660. }
  8661. .recognition-panel {
  8662. flex: 1 1 auto;
  8663. min-height: 0;
  8664. overflow: hidden;
  8665. display: flex;
  8666. flex-direction: column;
  8667. padding: 14px 16px 12px;
  8668. }
  8669. .recognition-panel-head {
  8670. display: flex;
  8671. align-items: center;
  8672. justify-content: space-between;
  8673. gap: 12px;
  8674. min-height: 32px;
  8675. }
  8676. .recognition-title {
  8677. color: #1f2937;
  8678. font-size: 14px;
  8679. font-weight: 600;
  8680. }
  8681. .recognition-toggle {
  8682. color: #3b5bdb;
  8683. font-size: 13px;
  8684. font-weight: 500;
  8685. cursor: pointer;
  8686. user-select: none;
  8687. white-space: nowrap;
  8688. display: inline-flex;
  8689. align-items: center;
  8690. gap: 4px;
  8691. }
  8692. .recognition-toggle-icon {
  8693. font-size: 11px;
  8694. line-height: 1;
  8695. }
  8696. .recognition-panel-body {
  8697. flex: 1 1 auto;
  8698. min-height: 0;
  8699. overflow: auto;
  8700. display: flex;
  8701. flex-direction: column;
  8702. gap: 12px;
  8703. margin-top: 12px;
  8704. padding-right: 4px;
  8705. }
  8706. .recognition-tabs {
  8707. display: flex;
  8708. align-items: center;
  8709. gap: 16px;
  8710. flex-wrap: wrap;
  8711. }
  8712. .recognition-tab {
  8713. position: relative;
  8714. color: #64748b;
  8715. font-size: 13px;
  8716. cursor: pointer;
  8717. transition: color 0.2s ease;
  8718. &::after {
  8719. content: '';
  8720. position: absolute;
  8721. left: 0;
  8722. right: 0;
  8723. bottom: -6px;
  8724. height: 2px;
  8725. border-radius: 999px;
  8726. background: transparent;
  8727. }
  8728. &.active {
  8729. color: #2f4fd2;
  8730. font-weight: 600;
  8731. &::after {
  8732. background: #2f4fd2;
  8733. }
  8734. }
  8735. }
  8736. .recognition-desc {
  8737. padding: 10px 12px;
  8738. border-radius: 10px;
  8739. background: #f7faff;
  8740. color: #64748b;
  8741. font-size: 12px;
  8742. line-height: 1.7;
  8743. }
  8744. .recognition-basic-shell {
  8745. display: flex;
  8746. flex-direction: column;
  8747. gap: 12px;
  8748. }
  8749. .recognition-basic-preview,
  8750. .recognition-basic-panel,
  8751. .recognition-basic-empty-shell,
  8752. .recognition-rule-placeholder {
  8753. border: 1px solid #dce6f3;
  8754. border-radius: 12px;
  8755. background: linear-gradient(180deg, #f9fbff 0%, #ffffff 100%);
  8756. box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
  8757. }
  8758. .recognition-basic-preview {
  8759. padding: 12px 14px;
  8760. }
  8761. .recognition-basic-preview__head {
  8762. display: flex;
  8763. align-items: center;
  8764. justify-content: space-between;
  8765. gap: 12px;
  8766. margin-bottom: 10px;
  8767. }
  8768. .recognition-basic-preview__title {
  8769. color: #1f2937;
  8770. font-size: 13px;
  8771. font-weight: 700;
  8772. }
  8773. .recognition-basic-preview__meta {
  8774. display: inline-flex;
  8775. align-items: center;
  8776. height: 24px;
  8777. padding: 0 10px;
  8778. border-radius: 999px;
  8779. background: #eef4ff;
  8780. color: #315efb;
  8781. font-size: 12px;
  8782. font-weight: 600;
  8783. white-space: nowrap;
  8784. }
  8785. .recognition-basic-preview__content {
  8786. min-height: 52px;
  8787. padding: 10px 12px;
  8788. border-radius: 10px;
  8789. background: #ffffff;
  8790. color: #475569;
  8791. font-size: 12px;
  8792. line-height: 1.6;
  8793. white-space: nowrap;
  8794. word-break: normal;
  8795. overflow-x: auto;
  8796. overflow-y: hidden;
  8797. border: 1px dashed #d7e2f3;
  8798. }
  8799. .recognition-basic-panel {
  8800. padding: 12px;
  8801. }
  8802. .recognition-basic-panel__head {
  8803. display: flex;
  8804. align-items: center;
  8805. justify-content: space-between;
  8806. gap: 12px;
  8807. margin-bottom: 10px;
  8808. }
  8809. .recognition-basic-panel__summary {
  8810. min-width: 0;
  8811. display: flex;
  8812. flex-direction: column;
  8813. gap: 4px;
  8814. }
  8815. .recognition-basic-panel__title-wrap {
  8816. display: flex;
  8817. align-items: center;
  8818. gap: 10px;
  8819. flex-wrap: wrap;
  8820. }
  8821. .recognition-basic-panel__title {
  8822. color: #111827;
  8823. font-size: 15px;
  8824. font-weight: 700;
  8825. }
  8826. .recognition-basic-panel__count {
  8827. display: inline-flex;
  8828. align-items: center;
  8829. height: 24px;
  8830. padding: 0 10px;
  8831. border-radius: 999px;
  8832. background: #eef4ff;
  8833. color: #315efb;
  8834. font-size: 12px;
  8835. font-weight: 700;
  8836. line-height: 24px;
  8837. }
  8838. .recognition-basic-panel__desc {
  8839. color: #6b7280;
  8840. font-size: 12px;
  8841. line-height: 1.6;
  8842. }
  8843. .recognition-basic-panel__actions {
  8844. display: flex;
  8845. align-items: center;
  8846. gap: 0;
  8847. flex-shrink: 0;
  8848. margin-left: auto;
  8849. }
  8850. .recognition-basic-panel__toggle-btn {
  8851. display: inline-flex;
  8852. align-items: center;
  8853. justify-content: center;
  8854. gap: 6px;
  8855. min-width: 76px;
  8856. height: 36px;
  8857. padding: 0 14px;
  8858. border-radius: 10px;
  8859. border: 1px solid #d7e4f3;
  8860. background: #fff;
  8861. color: #315efb;
  8862. font-weight: 600;
  8863. line-height: 1;
  8864. box-shadow: 0 4px 12px rgba(49, 94, 251, 0.08);
  8865. transition:
  8866. border-color 0.2s ease,
  8867. background-color 0.2s ease,
  8868. box-shadow 0.2s ease,
  8869. transform 0.2s ease;
  8870. &:hover,
  8871. &:focus {
  8872. border-color: #9db8ff;
  8873. background: #f7faff;
  8874. box-shadow: 0 6px 16px rgba(49, 94, 251, 0.14);
  8875. }
  8876. &:active {
  8877. transform: translateY(1px);
  8878. }
  8879. :deep(.ant-btn-icon) {
  8880. display: inline-flex;
  8881. align-items: center;
  8882. }
  8883. }
  8884. .recognition-basic-list {
  8885. display: flex;
  8886. flex-direction: column;
  8887. gap: 8px;
  8888. max-height: 220px;
  8889. overflow: auto;
  8890. padding-right: 2px;
  8891. }
  8892. .recognition-basic-item {
  8893. position: relative;
  8894. display: grid;
  8895. grid-template-columns: 48px minmax(0, 1fr);
  8896. align-items: center;
  8897. gap: 10px;
  8898. padding: 10px 12px;
  8899. border: 1px solid #dce5f1;
  8900. border-radius: 12px;
  8901. background: #fff;
  8902. box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
  8903. }
  8904. .recognition-basic-item__index {
  8905. display: inline-flex;
  8906. align-items: center;
  8907. justify-content: center;
  8908. width: 48px;
  8909. height: 40px;
  8910. border-radius: 10px;
  8911. background: linear-gradient(180deg, #f6f9fd 0%, #eef4ff 100%);
  8912. color: #315efb;
  8913. font-size: 13px;
  8914. font-weight: 700;
  8915. line-height: 1;
  8916. }
  8917. .recognition-basic-item__content {
  8918. display: grid;
  8919. grid-template-columns: repeat(3, minmax(0, 1fr));
  8920. align-items: center;
  8921. gap: 10px;
  8922. min-width: 0;
  8923. }
  8924. .recognition-basic-field {
  8925. display: flex;
  8926. flex-direction: column;
  8927. gap: 6px;
  8928. min-width: 0;
  8929. :deep(.ant-select),
  8930. :deep(.ant-input-affix-wrapper),
  8931. :deep(.ant-input) {
  8932. width: 100%;
  8933. }
  8934. :deep(.ant-input),
  8935. :deep(.ant-input-affix-wrapper),
  8936. :deep(.ant-select-selector) {
  8937. border-radius: 8px !important;
  8938. }
  8939. }
  8940. .recognition-basic-field__label {
  8941. color: #475569;
  8942. font-size: 12px;
  8943. font-weight: 600;
  8944. line-height: 1.4;
  8945. }
  8946. .recognition-basic-field__readonly {
  8947. display: flex;
  8948. align-items: center;
  8949. min-height: 32px;
  8950. padding: 0 12px;
  8951. border: 1px solid #d7e4f3;
  8952. border-radius: 8px;
  8953. background: linear-gradient(180deg, #f8fbff 0%, #f3f7ff 100%);
  8954. color: #1f2937;
  8955. font-size: 13px;
  8956. font-weight: 500;
  8957. line-height: 1.5;
  8958. white-space: nowrap;
  8959. overflow: hidden;
  8960. text-overflow: ellipsis;
  8961. &.is-empty {
  8962. color: #94a3b8;
  8963. }
  8964. }
  8965. .recognition-zf-shell {
  8966. display: flex;
  8967. flex-direction: column;
  8968. gap: 12px;
  8969. }
  8970. .recognition-zf-card {
  8971. display: flex;
  8972. align-items: center;
  8973. justify-content: space-between;
  8974. gap: 16px;
  8975. padding: 14px 16px;
  8976. border: 1px solid #dce6f3;
  8977. border-radius: 12px;
  8978. background: linear-gradient(180deg, #f9fbff 0%, #ffffff 100%);
  8979. box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
  8980. }
  8981. .recognition-zf-card__main {
  8982. min-width: 0;
  8983. display: flex;
  8984. flex-direction: column;
  8985. gap: 4px;
  8986. }
  8987. .recognition-zf-card__title {
  8988. color: #111827;
  8989. font-size: 15px;
  8990. font-weight: 700;
  8991. line-height: 1.5;
  8992. }
  8993. .recognition-zf-card__desc {
  8994. color: #6b7280;
  8995. font-size: 12px;
  8996. line-height: 1.7;
  8997. }
  8998. .recognition-direction-shell {
  8999. display: flex;
  9000. flex-direction: column;
  9001. gap: 12px;
  9002. padding: 14px 16px;
  9003. border: 1px solid #dce6f3;
  9004. border-radius: 12px;
  9005. background: linear-gradient(180deg, #f9fbff 0%, #ffffff 100%);
  9006. box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
  9007. }
  9008. .recognition-direction-shell__head {
  9009. display: flex;
  9010. align-items: flex-start;
  9011. justify-content: space-between;
  9012. gap: 12px;
  9013. }
  9014. .recognition-direction-shell__summary {
  9015. min-width: 0;
  9016. display: flex;
  9017. flex-direction: column;
  9018. gap: 4px;
  9019. }
  9020. .recognition-direction-shell__title {
  9021. color: #111827;
  9022. font-size: 15px;
  9023. font-weight: 700;
  9024. }
  9025. .recognition-direction-shell__desc {
  9026. color: #6b7280;
  9027. font-size: 12px;
  9028. line-height: 1.7;
  9029. }
  9030. .recognition-direction-shell__count {
  9031. display: inline-flex;
  9032. align-items: center;
  9033. height: 24px;
  9034. padding: 0 10px;
  9035. border-radius: 999px;
  9036. background: #eef4ff;
  9037. color: #315efb;
  9038. font-size: 12px;
  9039. font-weight: 700;
  9040. white-space: nowrap;
  9041. }
  9042. .recognition-direction-card-list {
  9043. display: flex;
  9044. flex-direction: column;
  9045. gap: 10px;
  9046. }
  9047. .recognition-direction-card {
  9048. padding: 12px;
  9049. border: 1px solid #dce5f1;
  9050. border-radius: 12px;
  9051. background: #fff;
  9052. box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
  9053. }
  9054. .recognition-direction-card__head {
  9055. display: flex;
  9056. align-items: center;
  9057. justify-content: space-between;
  9058. gap: 10px;
  9059. margin-bottom: 10px;
  9060. }
  9061. .recognition-direction-card__title {
  9062. color: #1f2937;
  9063. font-size: 13px;
  9064. font-weight: 700;
  9065. line-height: 1.5;
  9066. }
  9067. .recognition-direction-card__tag {
  9068. display: inline-flex;
  9069. align-items: center;
  9070. justify-content: center;
  9071. height: 24px;
  9072. padding: 0 10px;
  9073. border-radius: 999px;
  9074. background: #f3f6fb;
  9075. color: #475569;
  9076. font-size: 12px;
  9077. font-weight: 600;
  9078. white-space: nowrap;
  9079. }
  9080. .recognition-direction-list {
  9081. display: flex;
  9082. flex-direction: column;
  9083. gap: 8px;
  9084. }
  9085. .recognition-direction-row {
  9086. display: grid;
  9087. grid-template-columns: 72px minmax(0, 1fr);
  9088. align-items: center;
  9089. gap: 10px;
  9090. min-width: 0;
  9091. :deep(.ant-input),
  9092. :deep(.ant-input-affix-wrapper) {
  9093. width: 100%;
  9094. border-radius: 8px !important;
  9095. }
  9096. }
  9097. .recognition-direction-label {
  9098. display: inline-flex;
  9099. align-items: center;
  9100. justify-content: center;
  9101. min-height: 32px;
  9102. padding: 0 10px;
  9103. border-radius: 8px;
  9104. background: #f3f6fb;
  9105. color: #475569;
  9106. font-size: 12px;
  9107. font-weight: 600;
  9108. line-height: 1;
  9109. white-space: nowrap;
  9110. }
  9111. .recognition-direction-actions {
  9112. display: flex;
  9113. justify-content: flex-end;
  9114. gap: 10px;
  9115. padding-top: 4px;
  9116. }
  9117. .recognition-basic-empty,
  9118. .recognition-basic-empty-shell,
  9119. .recognition-rule-placeholder {
  9120. display: flex;
  9121. flex-direction: column;
  9122. align-items: flex-start;
  9123. gap: 8px;
  9124. }
  9125. .recognition-basic-empty {
  9126. padding: 12px 2px 2px;
  9127. }
  9128. .recognition-basic-empty-shell,
  9129. .recognition-rule-placeholder {
  9130. padding: 14px 16px;
  9131. }
  9132. .recognition-basic-empty__title,
  9133. .recognition-basic-empty-shell__title,
  9134. .recognition-rule-placeholder__title {
  9135. color: #1f2937;
  9136. font-size: 13px;
  9137. font-weight: 700;
  9138. line-height: 1.5;
  9139. }
  9140. .recognition-basic-empty__desc,
  9141. .recognition-basic-empty-shell__desc,
  9142. .recognition-rule-placeholder__desc {
  9143. color: #64748b;
  9144. font-size: 12px;
  9145. line-height: 1.7;
  9146. }
  9147. .child-template-panel {
  9148. flex: 0 0 auto;
  9149. padding: 12px 16px 14px;
  9150. border-top: 1px solid #eef2f7;
  9151. background: #fcfdff;
  9152. }
  9153. .template-foot-panel.is-compact {
  9154. .child-template-panel {
  9155. padding-top: 14px;
  9156. border-top: 0;
  9157. }
  9158. }
  9159. .child-template-tip {
  9160. color: #64748b;
  9161. font-size: 12px;
  9162. line-height: 1.7;
  9163. }
  9164. .child-template-form {
  9165. margin-top: 12px;
  9166. display: flex;
  9167. align-items: center;
  9168. gap: 10px;
  9169. :deep(.ant-input-affix-wrapper),
  9170. :deep(.ant-input) {
  9171. flex: 1;
  9172. }
  9173. }
  9174. .child-template-label {
  9175. color: #374151;
  9176. font-size: 14px;
  9177. white-space: nowrap;
  9178. }
  9179. .cleaning-rule-panel {
  9180. margin: 0;
  9181. border: 1px solid #e5e7eb;
  9182. border-radius: 14px;
  9183. padding: 12px;
  9184. background: #fff;
  9185. display: flex;
  9186. flex-direction: column;
  9187. min-height: 0;
  9188. flex-shrink: 0;
  9189. overflow: hidden;
  9190. transition:
  9191. padding 0.2s ease,
  9192. border-color 0.2s ease,
  9193. box-shadow 0.2s ease;
  9194. &.is-collapsed {
  9195. padding-bottom: 10px;
  9196. .rule-title-row {
  9197. margin-bottom: 0;
  9198. }
  9199. }
  9200. .rule-title-row {
  9201. display: flex;
  9202. align-items: center;
  9203. justify-content: space-between;
  9204. gap: 12px;
  9205. flex-wrap: wrap;
  9206. margin-bottom: 10px;
  9207. min-height: 36px;
  9208. flex: 0 0 auto;
  9209. .rule-tabs {
  9210. display: flex;
  9211. gap: 8px;
  9212. flex-wrap: wrap;
  9213. .rule-tab {
  9214. min-width: 82px;
  9215. height: 34px;
  9216. padding: 0 14px;
  9217. border: 1px solid #e5e7eb;
  9218. border-radius: 999px;
  9219. background: #fff;
  9220. color: #475569;
  9221. cursor: pointer;
  9222. transition: all 0.2s ease;
  9223. display: inline-flex;
  9224. align-items: center;
  9225. justify-content: center;
  9226. box-sizing: border-box;
  9227. &.active {
  9228. border-color: #c8d7fb;
  9229. background: #eef4ff;
  9230. color: #3651d9;
  9231. }
  9232. }
  9233. }
  9234. .rule-title-meta {
  9235. margin-left: auto;
  9236. display: inline-flex;
  9237. align-items: center;
  9238. justify-content: flex-end;
  9239. gap: 12px;
  9240. flex-wrap: wrap;
  9241. }
  9242. .current-col {
  9243. font-size: 12px;
  9244. color: #64748b;
  9245. }
  9246. .rule-toggle {
  9247. flex-shrink: 0;
  9248. display: inline-flex;
  9249. align-items: center;
  9250. gap: 4px;
  9251. height: 30px;
  9252. padding: 0 12px;
  9253. border: 1px solid #dbe4f0;
  9254. border-radius: 999px;
  9255. background: #fff;
  9256. color: #475569;
  9257. font-size: 12px;
  9258. font-weight: 600;
  9259. cursor: pointer;
  9260. user-select: none;
  9261. transition: all 0.2s ease;
  9262. &:hover,
  9263. &.expanded {
  9264. border-color: #c8d7fb;
  9265. background: #eef4ff;
  9266. color: #3651d9;
  9267. }
  9268. }
  9269. .rule-toggle-icon {
  9270. font-size: 11px;
  9271. line-height: 1;
  9272. }
  9273. }
  9274. .rule-content {
  9275. display: grid;
  9276. grid-template-columns: minmax(0, 0.95fr) 54px minmax(0, 0.95fr) minmax(360px, 1.55fr);
  9277. gap: 12px;
  9278. height: 392px;
  9279. min-height: 392px;
  9280. align-items: stretch;
  9281. .rule-col,
  9282. .rule-config {
  9283. border: 1px solid #e5e7eb;
  9284. border-radius: 10px;
  9285. overflow: hidden;
  9286. display: flex;
  9287. flex-direction: column;
  9288. min-height: 0;
  9289. }
  9290. .col-title {
  9291. height: 36px;
  9292. line-height: 36px;
  9293. padding: 0 10px;
  9294. background: #f8fafc;
  9295. font-size: 13px;
  9296. color: #334155;
  9297. font-weight: 600;
  9298. &.between {
  9299. display: flex;
  9300. justify-content: space-between;
  9301. }
  9302. .clear-btn {
  9303. color: #ef4444;
  9304. cursor: pointer;
  9305. font-weight: 400;
  9306. }
  9307. }
  9308. .col-list {
  9309. flex: 1;
  9310. min-height: 0;
  9311. overflow: auto;
  9312. padding: 4px 0;
  9313. .col-item {
  9314. height: 32px;
  9315. padding: 0 10px;
  9316. cursor: pointer;
  9317. color: #475569;
  9318. display: flex;
  9319. align-items: center;
  9320. justify-content: space-between;
  9321. gap: 8px;
  9322. &:hover {
  9323. background: #f8fafc;
  9324. }
  9325. &.active {
  9326. color: #3651d9;
  9327. background: #e8ebfb;
  9328. }
  9329. }
  9330. .col-item-label {
  9331. min-width: 0;
  9332. flex: 1 1 auto;
  9333. overflow: hidden;
  9334. text-overflow: ellipsis;
  9335. white-space: nowrap;
  9336. }
  9337. .col-item-status {
  9338. flex: 0 0 auto;
  9339. display: inline-flex;
  9340. align-items: center;
  9341. justify-content: center;
  9342. min-width: 52px;
  9343. height: 20px;
  9344. padding: 0 8px;
  9345. border-radius: 999px;
  9346. font-size: 11px;
  9347. font-weight: 600;
  9348. line-height: 1;
  9349. &.is-enabled {
  9350. background: #e8f8f1;
  9351. color: #0f766e;
  9352. }
  9353. &.is-disabled {
  9354. background: #f1f5f9;
  9355. color: #64748b;
  9356. }
  9357. }
  9358. }
  9359. .rule-op {
  9360. display: flex;
  9361. flex-direction: column;
  9362. align-items: center;
  9363. justify-content: center;
  9364. gap: 8px;
  9365. :deep(.ant-btn) {
  9366. width: 32px;
  9367. height: 28px;
  9368. padding: 0;
  9369. border-radius: 8px;
  9370. }
  9371. }
  9372. .config-placeholder {
  9373. flex: 1;
  9374. min-height: 0;
  9375. overflow: auto;
  9376. padding: 12px;
  9377. font-size: 13px;
  9378. color: #64748b;
  9379. background: #f8fbff;
  9380. display: flex;
  9381. flex-direction: column;
  9382. .config-rule-shell {
  9383. display: flex;
  9384. flex-direction: column;
  9385. gap: 12px;
  9386. min-height: 100%;
  9387. &.is-example-preview {
  9388. justify-content: center;
  9389. min-height: auto;
  9390. margin: auto 0;
  9391. }
  9392. }
  9393. .config-rule-head,
  9394. .config-section {
  9395. border: 1px solid #dbe7f6;
  9396. border-radius: 12px;
  9397. background: #fff;
  9398. box-shadow: 0 6px 18px rgba(148, 163, 184, 0.08);
  9399. }
  9400. .config-rule-head {
  9401. padding: 14px 16px;
  9402. display: flex;
  9403. flex-direction: column;
  9404. gap: 12px;
  9405. }
  9406. .config-rule-title-row {
  9407. display: flex;
  9408. align-items: flex-start;
  9409. justify-content: space-between;
  9410. gap: 12px;
  9411. }
  9412. .config-rule-copy {
  9413. min-width: 0;
  9414. flex: 1 1 auto;
  9415. }
  9416. .config-rule-title {
  9417. color: #0f172a;
  9418. font-size: 15px;
  9419. font-weight: 700;
  9420. line-height: 1.4;
  9421. }
  9422. .config-rule-desc {
  9423. margin-top: 6px;
  9424. color: #64748b;
  9425. line-height: 1.7;
  9426. }
  9427. .config-rule-meta {
  9428. display: inline-flex;
  9429. align-items: center;
  9430. justify-content: flex-end;
  9431. gap: 8px;
  9432. flex-wrap: wrap;
  9433. flex-shrink: 0;
  9434. }
  9435. .config-rule-state {
  9436. display: inline-flex;
  9437. align-items: center;
  9438. height: 28px;
  9439. padding: 0 10px;
  9440. border-radius: 999px;
  9441. font-size: 12px;
  9442. font-weight: 600;
  9443. &.is-enabled {
  9444. background: #e8f8f1;
  9445. color: #0f766e;
  9446. }
  9447. &.is-disabled {
  9448. background: #f1f5f9;
  9449. color: #64748b;
  9450. }
  9451. &.is-example {
  9452. background: #eff6ff;
  9453. color: #2563eb;
  9454. }
  9455. }
  9456. .config-rule-tag {
  9457. flex-shrink: 0;
  9458. display: inline-flex;
  9459. align-items: center;
  9460. height: 28px;
  9461. padding: 0 10px;
  9462. border-radius: 999px;
  9463. font-size: 12px;
  9464. font-weight: 600;
  9465. &.is-shared {
  9466. background: #eef2ff;
  9467. color: #4f46e5;
  9468. }
  9469. &.is-column {
  9470. background: #e9f8f4;
  9471. color: #0f766e;
  9472. }
  9473. }
  9474. .config-rule-toggle-btn {
  9475. border-radius: 999px;
  9476. }
  9477. .config-section {
  9478. padding: 14px 16px;
  9479. display: flex;
  9480. flex-direction: column;
  9481. gap: 12px;
  9482. &.is-example {
  9483. background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
  9484. }
  9485. }
  9486. .config-section-title {
  9487. color: #1e293b;
  9488. font-size: 13px;
  9489. font-weight: 700;
  9490. line-height: 1.4;
  9491. }
  9492. .config-form-grid {
  9493. display: grid;
  9494. grid-template-columns: repeat(2, minmax(0, 1fr));
  9495. gap: 10px;
  9496. }
  9497. .config-field {
  9498. display: flex;
  9499. flex-direction: column;
  9500. gap: 6px;
  9501. min-width: 0;
  9502. }
  9503. .config-field-label {
  9504. color: #475569;
  9505. font-size: 12px;
  9506. font-weight: 600;
  9507. line-height: 1.4;
  9508. }
  9509. .config-hint,
  9510. .config-desc-item {
  9511. color: #64748b;
  9512. line-height: 1.7;
  9513. }
  9514. .config-desc-list {
  9515. display: flex;
  9516. flex-direction: column;
  9517. gap: 6px;
  9518. }
  9519. .config-check-list {
  9520. display: flex;
  9521. flex-wrap: wrap;
  9522. gap: 10px 16px;
  9523. &.single {
  9524. gap: 0;
  9525. }
  9526. }
  9527. .config-check-inline {
  9528. display: grid;
  9529. grid-template-columns: auto minmax(0, 1fr);
  9530. gap: 10px;
  9531. align-items: center;
  9532. }
  9533. .config-extract-data-group {
  9534. display: grid;
  9535. grid-template-columns: repeat(2, minmax(0, 1fr));
  9536. gap: 12px;
  9537. }
  9538. .config-extract-data-item {
  9539. display: grid;
  9540. grid-template-columns: auto minmax(0, 1fr) auto;
  9541. gap: 8px;
  9542. align-items: center;
  9543. :deep(.ant-radio-wrapper) {
  9544. margin-inline-end: 0;
  9545. white-space: nowrap;
  9546. }
  9547. }
  9548. .config-extract-data-unit {
  9549. color: #475569;
  9550. white-space: nowrap;
  9551. }
  9552. .config-radio-group {
  9553. display: flex;
  9554. flex-wrap: wrap;
  9555. gap: 8px;
  9556. &.compact {
  9557. gap: 4px 12px;
  9558. }
  9559. :deep(.ant-radio-wrapper) {
  9560. margin-inline-end: 0;
  9561. }
  9562. }
  9563. .config-alert {
  9564. padding: 10px 12px;
  9565. border-radius: 10px;
  9566. background: #fff7ed;
  9567. border: 1px solid #fed7aa;
  9568. color: #c2410c;
  9569. line-height: 1.6;
  9570. }
  9571. .config-actions {
  9572. display: flex;
  9573. justify-content: flex-end;
  9574. gap: 8px;
  9575. &.between {
  9576. justify-content: space-between;
  9577. }
  9578. }
  9579. .grouping-list {
  9580. display: flex;
  9581. flex-direction: column;
  9582. gap: 10px;
  9583. }
  9584. .grouping-row {
  9585. display: grid;
  9586. grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(120px, 0.8fr) auto;
  9587. gap: 10px;
  9588. align-items: end;
  9589. }
  9590. .grouping-remove-btn {
  9591. align-self: end;
  9592. }
  9593. .config-example-list {
  9594. display: flex;
  9595. flex-direction: column;
  9596. gap: 8px;
  9597. }
  9598. .config-example-row {
  9599. display: grid;
  9600. grid-template-columns: 72px minmax(0, 1fr);
  9601. gap: 10px;
  9602. align-items: start;
  9603. color: #475569;
  9604. line-height: 1.7;
  9605. span:first-child {
  9606. color: #64748b;
  9607. font-weight: 600;
  9608. }
  9609. code {
  9610. padding: 4px 8px;
  9611. border-radius: 8px;
  9612. background: #eff6ff;
  9613. color: #1d4ed8;
  9614. word-break: break-all;
  9615. }
  9616. }
  9617. .config-preview-tip {
  9618. padding: 10px 12px;
  9619. border: 1px dashed #c7d8f6;
  9620. border-radius: 10px;
  9621. background: #fff;
  9622. color: #64748b;
  9623. line-height: 1.7;
  9624. }
  9625. :deep(.ant-select),
  9626. :deep(.ant-input-number) {
  9627. width: 100%;
  9628. }
  9629. :deep(.ant-input-number) {
  9630. border-radius: 8px;
  9631. }
  9632. .config-empty-state {
  9633. margin-top: auto;
  9634. margin-bottom: auto;
  9635. padding: 14px;
  9636. border: 1px dashed #d7e2f3;
  9637. border-radius: 10px;
  9638. background: #fff;
  9639. }
  9640. .empty-title {
  9641. color: #111827;
  9642. font-size: 14px;
  9643. font-weight: 600;
  9644. }
  9645. .empty-text {
  9646. margin-top: 8px;
  9647. color: #475569;
  9648. line-height: 1.7;
  9649. }
  9650. .empty-tip {
  9651. margin-top: 8px;
  9652. color: #6b7280;
  9653. line-height: 1.7;
  9654. }
  9655. }
  9656. @media (max-width: 1680px) {
  9657. grid-template-columns: minmax(0, 1fr) 48px minmax(0, 1fr);
  9658. height: auto;
  9659. min-height: 0;
  9660. .rule-config {
  9661. grid-column: 1 / -1;
  9662. min-height: 340px;
  9663. }
  9664. }
  9665. @media (max-width: 1320px) {
  9666. .config-placeholder {
  9667. .config-form-grid,
  9668. .grouping-row {
  9669. grid-template-columns: 1fr;
  9670. }
  9671. .config-actions.between {
  9672. flex-direction: column;
  9673. align-items: stretch;
  9674. }
  9675. .grouping-remove-btn {
  9676. width: 100%;
  9677. }
  9678. }
  9679. }
  9680. }
  9681. @media (max-width: 1320px) {
  9682. .rule-title-row {
  9683. align-items: flex-start;
  9684. .rule-title-meta {
  9685. width: 100%;
  9686. justify-content: space-between;
  9687. }
  9688. }
  9689. }
  9690. }
  9691. .table-mock {
  9692. margin: 0;
  9693. border: 1px solid #e5e7eb;
  9694. border-radius: 14px;
  9695. min-height: 0;
  9696. height: 100%;
  9697. overflow: auto;
  9698. overscroll-behavior: contain;
  9699. .mock-head,
  9700. .mock-match-row,
  9701. .mock-row {
  9702. display: grid;
  9703. width: max-content;
  9704. min-width: max-content;
  9705. }
  9706. .mock-head {
  9707. background: #f8fafc;
  9708. color: #334155;
  9709. font-weight: 600;
  9710. > span {
  9711. gap: 8px;
  9712. }
  9713. span {
  9714. cursor: pointer;
  9715. }
  9716. span.is-added {
  9717. color: #3651d9;
  9718. }
  9719. span.active {
  9720. background: #eef4ff;
  9721. color: #3651d9;
  9722. }
  9723. }
  9724. .mock-match-row {
  9725. border-top: 1px solid #e5e7eb;
  9726. background: #f8fbff;
  9727. color: #334155;
  9728. > span {
  9729. border-top: 0;
  9730. }
  9731. .match-field-content {
  9732. display: inline-flex;
  9733. align-items: center;
  9734. gap: 6px;
  9735. width: auto;
  9736. max-width: 100%;
  9737. min-width: 0;
  9738. overflow: hidden;
  9739. }
  9740. .match-status-group {
  9741. display: inline-flex;
  9742. align-items: center;
  9743. gap: 6px;
  9744. flex: 0 0 auto;
  9745. }
  9746. .match-status-badge {
  9747. width: 16px;
  9748. height: 16px;
  9749. border-radius: 50%;
  9750. display: inline-flex;
  9751. align-items: center;
  9752. justify-content: center;
  9753. flex: 0 0 16px;
  9754. color: #fff;
  9755. font-size: 11px;
  9756. font-weight: 700;
  9757. &.is-success {
  9758. background: #22c55e;
  9759. }
  9760. &.is-fail {
  9761. background: #ef4444;
  9762. }
  9763. &.is-formatError {
  9764. background: #f59e0b;
  9765. }
  9766. }
  9767. .match-field-text {
  9768. color: #3651d9;
  9769. font-weight: 600;
  9770. display: block;
  9771. flex: 0 1 auto;
  9772. min-width: 0;
  9773. max-width: calc(100% - 22px);
  9774. overflow: hidden;
  9775. text-overflow: ellipsis;
  9776. white-space: nowrap;
  9777. }
  9778. span.is-empty .match-field-text {
  9779. color: #94a3b8;
  9780. font-weight: 500;
  9781. }
  9782. > span.is-empty .match-field-content {
  9783. width: auto;
  9784. min-width: auto;
  9785. overflow: visible;
  9786. }
  9787. > span.is-empty .match-field-text {
  9788. flex: 0 0 auto;
  9789. max-width: none;
  9790. }
  9791. > span:not(.is-empty) .match-field-content {
  9792. padding-right: 0;
  9793. }
  9794. > span:not(.is-empty) .match-status-group {
  9795. position: static;
  9796. transform: none;
  9797. }
  9798. }
  9799. .mock-row {
  9800. border-top: 1px solid #f1f5f9;
  9801. color: #475569;
  9802. position: relative;
  9803. transition:
  9804. box-shadow 0.2s ease,
  9805. background-color 0.2s ease;
  9806. &::after {
  9807. content: '';
  9808. position: absolute;
  9809. inset: 0;
  9810. background: rgba(54, 81, 217, 0.14);
  9811. opacity: 0;
  9812. pointer-events: none;
  9813. transition: opacity 0.2s ease;
  9814. }
  9815. &.is-current-row {
  9816. background: #eef4ff;
  9817. box-shadow:
  9818. inset 3px 0 0 #3651d9,
  9819. inset 0 0 0 1px rgba(54, 81, 217, 0.24);
  9820. &::after {
  9821. opacity: 1;
  9822. }
  9823. span {
  9824. background: transparent;
  9825. box-shadow: none;
  9826. color: inherit;
  9827. }
  9828. }
  9829. &.is-header-row {
  9830. background: #f3f7ff;
  9831. box-shadow:
  9832. inset 0 1px 0 rgba(54, 81, 217, 0.16),
  9833. inset 0 -1px 0 rgba(54, 81, 217, 0.16);
  9834. span {
  9835. font-weight: 600;
  9836. color: #274690;
  9837. }
  9838. }
  9839. }
  9840. .mock-row.is-header-row.is-current-row {
  9841. background: #e8f0ff;
  9842. }
  9843. .mock-row.is-ignored-row {
  9844. background: #5f6368;
  9845. color: #fff;
  9846. }
  9847. .mock-row.is-ignored-row.is-header-row {
  9848. background: #48505d;
  9849. span {
  9850. color: #fff;
  9851. }
  9852. }
  9853. .mock-head > span,
  9854. .mock-match-row > span,
  9855. .mock-row > span {
  9856. padding: 10px 12px;
  9857. font-size: 12px;
  9858. cursor: pointer;
  9859. white-space: nowrap;
  9860. overflow: hidden;
  9861. text-overflow: ellipsis;
  9862. min-width: 0;
  9863. min-height: 44px;
  9864. display: flex;
  9865. align-items: center;
  9866. position: relative;
  9867. z-index: 1;
  9868. box-sizing: border-box;
  9869. transition:
  9870. background-color 0.2s ease,
  9871. box-shadow 0.2s ease,
  9872. color 0.2s ease;
  9873. }
  9874. .row-index-cell {
  9875. justify-content: center;
  9876. text-align: center;
  9877. color: #64748b;
  9878. font-weight: 600;
  9879. }
  9880. .row-index-head {
  9881. color: #475569;
  9882. background: #f8fafc;
  9883. }
  9884. .match-row-index {
  9885. background: #f8fbff;
  9886. }
  9887. .rule-badge {
  9888. position: relative;
  9889. flex: 0 0 auto;
  9890. width: 24px;
  9891. height: 14px;
  9892. margin-left: 8px;
  9893. border-radius: 999px;
  9894. background: linear-gradient(180deg, #eef4ff 0%, #e1ebff 100%);
  9895. box-shadow: inset 0 0 0 1px rgba(54, 81, 217, 0.14);
  9896. &::before {
  9897. content: '';
  9898. position: absolute;
  9899. left: 5px;
  9900. right: 5px;
  9901. top: 50%;
  9902. height: 2px;
  9903. border-radius: 999px;
  9904. background: currentColor;
  9905. opacity: 0.38;
  9906. transform: translateY(-50%);
  9907. }
  9908. &::after {
  9909. content: '';
  9910. position: absolute;
  9911. left: 4px;
  9912. top: 50%;
  9913. width: 4px;
  9914. height: 4px;
  9915. border-radius: 50%;
  9916. background: currentColor;
  9917. transform: translateY(-50%);
  9918. box-shadow:
  9919. 6px 0 0 currentColor,
  9920. 12px 0 0 currentColor;
  9921. }
  9922. &.is-active {
  9923. color: #16a34a;
  9924. background: linear-gradient(180deg, #edfdf3 0%, #ddf8e8 100%);
  9925. box-shadow: inset 0 0 0 1px rgba(22, 163, 74, 0.18);
  9926. }
  9927. &.is-draft {
  9928. color: #64748b;
  9929. background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
  9930. box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.22);
  9931. }
  9932. }
  9933. .mock-head span.is-current-col {
  9934. background: #dfe8ff;
  9935. box-shadow:
  9936. inset 0 0 0 1px #3651d9,
  9937. inset 0 -2px 0 #3651d9;
  9938. color: #3651d9;
  9939. }
  9940. .mock-head span.is-current-col.is-first-col {
  9941. border-top-left-radius: 14px;
  9942. }
  9943. .mock-head span.is-current-col.is-last-col {
  9944. border-top-right-radius: 14px;
  9945. }
  9946. .mock-row span.is-current-col {
  9947. background: rgba(54, 81, 217, 0.16) !important;
  9948. box-shadow:
  9949. inset 1px 0 0 rgba(54, 81, 217, 0.24),
  9950. inset -1px 0 0 rgba(54, 81, 217, 0.24) !important;
  9951. color: #243b7a !important;
  9952. }
  9953. .mock-match-row span.is-current-col {
  9954. background: rgba(54, 81, 217, 0.12);
  9955. box-shadow:
  9956. inset 1px 0 0 rgba(54, 81, 217, 0.24),
  9957. inset -1px 0 0 rgba(54, 81, 217, 0.24);
  9958. }
  9959. .mock-row.is-current-row span.is-current-col {
  9960. background: rgba(54, 81, 217, 0.22) !important;
  9961. box-shadow:
  9962. inset 1px 0 0 rgba(54, 81, 217, 0.28),
  9963. inset -1px 0 0 rgba(54, 81, 217, 0.28) !important;
  9964. color: #1f3fbf !important;
  9965. }
  9966. .mock-row.is-ignored-row span.is-current-col {
  9967. background: rgba(255, 255, 255, 0.12) !important;
  9968. box-shadow:
  9969. inset 1px 0 0 rgba(255, 255, 255, 0.2),
  9970. inset -1px 0 0 rgba(255, 255, 255, 0.2) !important;
  9971. color: #fff !important;
  9972. }
  9973. }
  9974. @media (max-width: 1180px) {
  9975. .page-header-main {
  9976. flex-direction: column;
  9977. }
  9978. .page-header-actions {
  9979. width: 100%;
  9980. margin-left: 0;
  9981. justify-content: flex-start;
  9982. }
  9983. .template-match-bar,
  9984. .mapping-card-head,
  9985. .child-template-form {
  9986. align-items: flex-start;
  9987. flex-direction: column;
  9988. }
  9989. .middle-empty-stage-head {
  9990. align-items: flex-start;
  9991. flex-direction: column;
  9992. }
  9993. .middle-empty-stage {
  9994. min-height: 320px;
  9995. }
  9996. .template-foot-panel {
  9997. flex-basis: clamp(316px, 54%, 480px);
  9998. min-height: clamp(316px, 54%, 480px);
  9999. max-height: clamp(316px, 54%, 480px);
  10000. &.is-collapsed {
  10001. flex-basis: 208px;
  10002. min-height: 208px;
  10003. max-height: 208px;
  10004. }
  10005. &.is-compact {
  10006. flex-basis: 116px;
  10007. min-height: 116px;
  10008. max-height: 116px;
  10009. }
  10010. }
  10011. .middle-empty-highlights {
  10012. grid-template-columns: 1fr;
  10013. }
  10014. .right-panel-body {
  10015. gap: 10px;
  10016. padding: 10px 12px 12px;
  10017. }
  10018. .toolbar--column-actions {
  10019. align-items: flex-start;
  10020. .toolbar-head,
  10021. .toolbar-actions,
  10022. .toolbar-meta {
  10023. width: 100%;
  10024. }
  10025. .toolbar-label {
  10026. padding-right: 0;
  10027. }
  10028. .toolbar-label::after {
  10029. display: none;
  10030. }
  10031. .toolbar-meta {
  10032. justify-content: flex-start;
  10033. }
  10034. }
  10035. .recognition-basic-panel__head,
  10036. .recognition-basic-panel__actions,
  10037. .recognition-basic-preview__head,
  10038. .recognition-zf-card,
  10039. .recognition-direction-shell__head {
  10040. align-items: flex-start;
  10041. flex-direction: column;
  10042. }
  10043. .recognition-basic-panel__actions,
  10044. .recognition-direction-actions {
  10045. width: auto;
  10046. justify-content: flex-end;
  10047. }
  10048. .recognition-basic-item {
  10049. grid-template-columns: 1fr;
  10050. }
  10051. .recognition-basic-item__index {
  10052. width: 40px;
  10053. }
  10054. .recognition-basic-item__content {
  10055. grid-template-columns: 1fr;
  10056. }
  10057. .recognition-direction-row {
  10058. grid-template-columns: 1fr;
  10059. }
  10060. .recognition-direction-label {
  10061. justify-content: flex-start;
  10062. }
  10063. }
  10064. }
  10065. // Modal 样式需要在 .case-data-cleaning-page 外部,因为 Modal 会渲染到 body 下
  10066. .cleaning-config-modal {
  10067. .ant-modal-content {
  10068. border-radius: 14px;
  10069. padding: 0;
  10070. overflow: hidden;
  10071. border: 1px solid #e5e7eb;
  10072. box-shadow: 0 18px 40px rgba(15, 23, 42, 0.14);
  10073. }
  10074. .ant-modal-header {
  10075. padding: 12px 16px;
  10076. border-bottom: 1px solid #e5e7eb;
  10077. background: @modal-header-bg-color;
  10078. }
  10079. .ant-modal-title {
  10080. color: #111827;
  10081. font-size: 16px;
  10082. font-weight: 600;
  10083. line-height: 24px;
  10084. }
  10085. .ant-modal-body {
  10086. padding: 16px;
  10087. background: #fff;
  10088. }
  10089. .ant-modal-footer {
  10090. padding: 8px 16px 10px;
  10091. border-top: 1px solid #e5e7eb;
  10092. background: #fff;
  10093. }
  10094. .ant-btn {
  10095. min-width: 72px;
  10096. height: 36px;
  10097. padding: 0 18px;
  10098. border-radius: 18px;
  10099. border-width: 1px;
  10100. font-size: 14px;
  10101. font-weight: 500;
  10102. }
  10103. .ant-btn-primary {
  10104. box-shadow: none;
  10105. }
  10106. .ant-input,
  10107. .ant-input-affix-wrapper,
  10108. .ant-select-selector {
  10109. border-radius: 8px !important;
  10110. }
  10111. }
  10112. .cleaning-modal-title {
  10113. display: flex;
  10114. flex-direction: column;
  10115. gap: 4px;
  10116. .title-main {
  10117. font-size: 16px;
  10118. font-weight: 600;
  10119. color: #111827;
  10120. }
  10121. .title-desc {
  10122. font-size: 12px;
  10123. line-height: 1.6;
  10124. color: #6b7280;
  10125. font-weight: 400;
  10126. }
  10127. }
  10128. .split-column-modal {
  10129. .ant-modal {
  10130. width: calc(100vw - 32px) !important;
  10131. max-width: 460px;
  10132. }
  10133. }
  10134. .split-column-form {
  10135. display: flex;
  10136. flex-direction: column;
  10137. gap: 12px;
  10138. .split-column-tip {
  10139. padding: 10px 12px;
  10140. border: 1px dashed #d6deee;
  10141. border-radius: 10px;
  10142. background: #f8fbff;
  10143. color: #64748b;
  10144. font-size: 12px;
  10145. line-height: 1.6;
  10146. }
  10147. .split-form-card {
  10148. padding: 16px 16px 2px;
  10149. border: 1px solid #e8edf5;
  10150. border-radius: 12px;
  10151. background: #f8fafc;
  10152. }
  10153. .form-row {
  10154. display: flex;
  10155. align-items: center;
  10156. gap: 12px;
  10157. margin-bottom: 14px;
  10158. }
  10159. .form-label {
  10160. width: 126px;
  10161. flex-shrink: 0;
  10162. color: #374151;
  10163. font-size: 14px;
  10164. text-align: right;
  10165. &.required::before {
  10166. content: '*';
  10167. color: #ff4d4f;
  10168. margin-right: 4px;
  10169. }
  10170. }
  10171. .form-input {
  10172. flex: 1;
  10173. max-width: 292px;
  10174. }
  10175. .delimiter-group {
  10176. display: flex;
  10177. gap: 8px;
  10178. flex: 1;
  10179. max-width: 292px;
  10180. }
  10181. .delimiter-select {
  10182. width: 120px;
  10183. flex-shrink: 0;
  10184. }
  10185. .delimiter-input {
  10186. flex: 1;
  10187. min-width: 0;
  10188. }
  10189. .form-row:last-child {
  10190. margin-bottom: 0;
  10191. }
  10192. }
  10193. .add-column-modal {
  10194. .ant-modal {
  10195. width: calc(100vw - 32px) !important;
  10196. max-width: 704px;
  10197. }
  10198. }
  10199. .add-col-dialog {
  10200. .label.required::before {
  10201. content: '*';
  10202. color: #ef4444;
  10203. margin-right: 4px;
  10204. }
  10205. &.addColumn {
  10206. .addColumn-name {
  10207. margin-bottom: 16px;
  10208. padding: 14px 16px;
  10209. border: 1px solid #e8edf5;
  10210. border-radius: 12px;
  10211. background: #f8fafc;
  10212. .row {
  10213. display: flex;
  10214. align-items: center;
  10215. gap: 12px;
  10216. width: 100%;
  10217. flex-wrap: nowrap;
  10218. }
  10219. .label {
  10220. width: 92px;
  10221. min-width: 92px;
  10222. flex: 0 0 92px;
  10223. color: #374151;
  10224. font-size: 14px;
  10225. line-height: 32px;
  10226. font-weight: 500;
  10227. white-space: nowrap;
  10228. &::before {
  10229. content: '*';
  10230. color: #e65050;
  10231. margin-right: 4px;
  10232. }
  10233. }
  10234. .name-input {
  10235. flex: 1 1 auto;
  10236. min-width: 0;
  10237. }
  10238. .name-input:deep(.ant-input-affix-wrapper),
  10239. .name-input:deep(.ant-input) {
  10240. width: 100%;
  10241. height: 32px;
  10242. line-height: 32px;
  10243. border-radius: 4px;
  10244. font-size: 14px;
  10245. }
  10246. }
  10247. .addColumn-tip {
  10248. margin-bottom: 12px;
  10249. padding: 10px 12px;
  10250. border: 1px dashed #d6deee;
  10251. border-radius: 10px;
  10252. background: #f8fbff;
  10253. color: #64748b;
  10254. font-size: 12px;
  10255. line-height: 1.6;
  10256. }
  10257. .addColumn-content {
  10258. display: flex;
  10259. gap: 18px;
  10260. height: 404px;
  10261. .addColumn-content-left,
  10262. .addColumn-content-right {
  10263. display: flex;
  10264. flex-direction: column;
  10265. min-width: 0;
  10266. padding: 16px;
  10267. border: 1px solid #e8edf5;
  10268. border-radius: 12px;
  10269. background: #fff;
  10270. }
  10271. .addColumn-content-left {
  10272. flex: 0 0 42%;
  10273. }
  10274. .addColumn-content-right {
  10275. flex: 1 1 58%;
  10276. }
  10277. .content-card-head {
  10278. display: flex;
  10279. flex-direction: column;
  10280. gap: 4px;
  10281. min-width: 0;
  10282. }
  10283. .content-left-tip,
  10284. .content-right-tip {
  10285. color: #111827;
  10286. font-size: 14px;
  10287. font-weight: 600;
  10288. line-height: 22px;
  10289. }
  10290. .content-card-desc {
  10291. color: #6b7280;
  10292. font-size: 12px;
  10293. line-height: 1.5;
  10294. }
  10295. .left-oldColumn,
  10296. .right-newColumn {
  10297. flex: 1;
  10298. min-height: 0;
  10299. overflow-y: auto;
  10300. border: 1px solid #e5e7eb;
  10301. border-radius: 10px;
  10302. margin-top: 12px;
  10303. background: #f8fafc;
  10304. padding: 6px 0;
  10305. }
  10306. .left-oldColumn::-webkit-scrollbar,
  10307. .right-newColumn::-webkit-scrollbar {
  10308. width: 8px;
  10309. }
  10310. .left-oldColumn::-webkit-scrollbar-thumb,
  10311. .right-newColumn::-webkit-scrollbar-thumb {
  10312. background: #9ca3af;
  10313. border-radius: 999px;
  10314. }
  10315. .oldColumn-box,
  10316. .newColumn-box {
  10317. min-height: 36px;
  10318. padding: 0 14px;
  10319. color: #475569;
  10320. font-size: 14px;
  10321. cursor: pointer;
  10322. display: flex;
  10323. align-items: center;
  10324. justify-content: space-between;
  10325. border: 1px solid transparent;
  10326. border-radius: 8px;
  10327. margin: 0 8px 4px;
  10328. transition: all 0.2s ease;
  10329. &:hover {
  10330. background: #f3f6fb;
  10331. color: #1f2937;
  10332. }
  10333. &.active-item {
  10334. background: #eef4ff;
  10335. color: #1d4ed8;
  10336. font-weight: 600;
  10337. }
  10338. }
  10339. .oldColumn-box {
  10340. white-space: nowrap;
  10341. overflow: hidden;
  10342. text-overflow: ellipsis;
  10343. }
  10344. .right-header {
  10345. display: flex;
  10346. justify-content: space-between;
  10347. align-items: flex-start;
  10348. gap: 12px;
  10349. .content-card-head {
  10350. flex: 1;
  10351. }
  10352. }
  10353. .right-radio {
  10354. margin: 12px 0 10px;
  10355. padding: 10px 12px;
  10356. border: 1px solid #e8edf5;
  10357. border-radius: 10px;
  10358. background: #f8fafc;
  10359. :deep(.ant-radio-wrapper) {
  10360. margin-right: 24px;
  10361. color: #475569;
  10362. font-size: 14px;
  10363. line-height: 1;
  10364. }
  10365. :deep(.ant-radio-inner) {
  10366. width: 16px;
  10367. height: 16px;
  10368. }
  10369. }
  10370. .newColumn-box {
  10371. cursor: grab;
  10372. user-select: none;
  10373. &:active {
  10374. cursor: grabbing;
  10375. }
  10376. &.is-dragging {
  10377. opacity: 0.72;
  10378. transform: scale(0.995);
  10379. box-shadow: 0 8px 18px rgba(59, 130, 246, 0.12);
  10380. }
  10381. &.is-drag-over {
  10382. color: #1d4ed8;
  10383. border-color: #bfdbfe;
  10384. background: linear-gradient(180deg, #eff6ff 0%, #eef4ff 100%);
  10385. }
  10386. .newColumn-text {
  10387. flex: 1;
  10388. min-width: 0;
  10389. white-space: nowrap;
  10390. overflow: hidden;
  10391. text-overflow: ellipsis;
  10392. margin-right: 10px;
  10393. }
  10394. .del-icon {
  10395. font-size: 16px;
  10396. opacity: 1;
  10397. line-height: 1;
  10398. color: inherit;
  10399. }
  10400. }
  10401. .right-input {
  10402. margin-top: 10px;
  10403. :deep(.ant-input) {
  10404. height: 32px;
  10405. line-height: 32px;
  10406. font-size: 14px;
  10407. }
  10408. }
  10409. }
  10410. .dialog-footer {
  10411. margin-top: 18px;
  10412. padding-top: 16px;
  10413. border-top: 1px solid #eef2f7;
  10414. display: flex;
  10415. justify-content: flex-end;
  10416. gap: 12px;
  10417. :deep(.ant-btn) {
  10418. min-width: 80px;
  10419. height: 40px;
  10420. padding: 0 20px;
  10421. border-radius: 20px;
  10422. border-width: 1px;
  10423. font-size: 14px;
  10424. font-weight: 500;
  10425. box-shadow: none;
  10426. }
  10427. :deep(.ant-btn.add-col-confirm) {
  10428. border-color: #3651d9;
  10429. background: #3651d9;
  10430. color: #fff;
  10431. }
  10432. :deep(.ant-btn.add-col-cancel) {
  10433. border-color: #d1d5db;
  10434. color: #475569;
  10435. background: #fff;
  10436. }
  10437. }
  10438. }
  10439. }
  10440. body.is-resizing-layout,
  10441. body.is-resizing-layout * {
  10442. user-select: none !important;
  10443. -webkit-user-select: none !important;
  10444. }
  10445. .origin-field-popover {
  10446. .ant-popover-inner {
  10447. padding: 0;
  10448. border-radius: 12px;
  10449. border: 1px solid #e5e7eb;
  10450. box-shadow: 0 16px 32px rgba(15, 23, 42, 0.12);
  10451. }
  10452. .ant-popover-inner-content {
  10453. padding: 0;
  10454. }
  10455. .origin-pop {
  10456. width: 430px;
  10457. padding: 14px;
  10458. .origin-pop-title {
  10459. display: flex;
  10460. align-items: center;
  10461. gap: 12px;
  10462. .small-title {
  10463. font-size: 15px;
  10464. font-weight: 600;
  10465. white-space: nowrap;
  10466. color: #111827;
  10467. }
  10468. }
  10469. .origin-pop-list {
  10470. margin-top: 12px;
  10471. max-height: 260px;
  10472. overflow: auto;
  10473. display: grid;
  10474. grid-template-columns: repeat(2, minmax(0, 1fr));
  10475. gap: 10px 12px;
  10476. }
  10477. .origin-tag-wrap {
  10478. display: flex;
  10479. align-items: center;
  10480. gap: 6px;
  10481. }
  10482. .origin-tag {
  10483. height: 34px;
  10484. line-height: 32px;
  10485. text-align: center;
  10486. border-radius: 8px;
  10487. background: #fff;
  10488. border: 1px solid #e5e7eb;
  10489. cursor: pointer;
  10490. width: 139px;
  10491. box-sizing: border-box;
  10492. padding: 0 10px;
  10493. overflow: hidden;
  10494. text-overflow: ellipsis;
  10495. white-space: nowrap;
  10496. color: #475569;
  10497. &:hover {
  10498. background: #f8fbff;
  10499. border-color: #c8d7fb;
  10500. color: #1d4ed8;
  10501. }
  10502. &.blank {
  10503. background: #eef4ff;
  10504. border-color: #c8d7fb;
  10505. color: #1d4ed8;
  10506. }
  10507. &.disabled {
  10508. color: #94a3b8;
  10509. background: #f3f4f6;
  10510. border-color: #e5e7eb;
  10511. cursor: not-allowed;
  10512. }
  10513. &.active {
  10514. color: #1d4ed8;
  10515. border-color: #c8d7fb;
  10516. background: #eef4ff;
  10517. }
  10518. }
  10519. .replace {
  10520. color: #3651d9;
  10521. cursor: pointer;
  10522. font-size: 14px;
  10523. }
  10524. .origin-pop-bottom {
  10525. margin-top: 10px;
  10526. text-align: right;
  10527. color: #3651d9;
  10528. cursor: pointer;
  10529. }
  10530. }
  10531. }
  10532. .select-template-modal {
  10533. .ant-modal {
  10534. width: calc(100vw - 32px) !important;
  10535. max-width: 1080px;
  10536. }
  10537. .ant-modal-content {
  10538. border-radius: 14px;
  10539. overflow: hidden;
  10540. border: 1px solid #e5e7eb;
  10541. box-shadow: 0 18px 40px rgba(15, 23, 42, 0.14);
  10542. }
  10543. .ant-modal-header {
  10544. padding: 10px 16px;
  10545. border-bottom: 1px solid #eef2f7;
  10546. background: @modal-header-bg-color;
  10547. }
  10548. .ant-modal-body {
  10549. padding: 10px;
  10550. background: #f8fafc;
  10551. }
  10552. .template-modal-layout {
  10553. display: grid;
  10554. grid-template-columns: 200px 460px 1fr;
  10555. gap: 12px;
  10556. height: 620px;
  10557. }
  10558. .modal-left,
  10559. .modal-mid,
  10560. .modal-right {
  10561. border: 1px solid #e5e7eb;
  10562. border-radius: 14px;
  10563. background: #fff;
  10564. min-height: 0;
  10565. overflow: hidden;
  10566. box-shadow: 0 10px 26px rgba(15, 23, 42, 0.04);
  10567. }
  10568. .modal-panel-head {
  10569. display: flex;
  10570. align-items: flex-start;
  10571. justify-content: space-between;
  10572. gap: 12px;
  10573. margin-bottom: 10px;
  10574. .modal-panel-head__actions {
  10575. display: flex;
  10576. align-items: center;
  10577. gap: 10px;
  10578. flex-shrink: 0;
  10579. }
  10580. .panel-title {
  10581. font-size: 15px;
  10582. font-weight: 600;
  10583. color: #111827;
  10584. }
  10585. .head-badge {
  10586. flex-shrink: 0;
  10587. padding: 4px 10px;
  10588. border-radius: 999px;
  10589. border: 1px solid #dbe7ff;
  10590. background: #eef4ff;
  10591. color: #3651d9;
  10592. font-size: 12px;
  10593. line-height: 1;
  10594. font-weight: 600;
  10595. }
  10596. }
  10597. .modal-left {
  10598. padding: 10px;
  10599. display: flex;
  10600. flex-direction: column;
  10601. .create-template-btn {
  10602. height: 34px;
  10603. border-radius: 10px;
  10604. margin-bottom: 10px;
  10605. }
  10606. .cate-list {
  10607. flex: 1;
  10608. overflow: auto;
  10609. display: flex;
  10610. flex-direction: column;
  10611. gap: 8px;
  10612. }
  10613. .cate-item {
  10614. display: flex;
  10615. align-items: center;
  10616. justify-content: space-between;
  10617. padding: 10px 12px;
  10618. border-radius: 10px;
  10619. border: 1px solid #edf2f7;
  10620. background: #fff;
  10621. cursor: pointer;
  10622. transition: all 0.2s ease;
  10623. .cate-name {
  10624. min-width: 0;
  10625. overflow: hidden;
  10626. text-overflow: ellipsis;
  10627. white-space: nowrap;
  10628. color: #334155;
  10629. font-size: 13px;
  10630. font-weight: 500;
  10631. }
  10632. .cate-count {
  10633. flex-shrink: 0;
  10634. min-width: 24px;
  10635. height: 24px;
  10636. padding: 0 8px;
  10637. border-radius: 999px;
  10638. background: #f8fafc;
  10639. color: #64748b;
  10640. font-size: 12px;
  10641. line-height: 24px;
  10642. text-align: center;
  10643. }
  10644. &:hover {
  10645. border-color: #dbe7ff;
  10646. background: #f8fbff;
  10647. }
  10648. &.active {
  10649. border-color: #c8d7fb;
  10650. background: #eef4ff;
  10651. .cate-name,
  10652. .cate-count {
  10653. color: #3651d9;
  10654. }
  10655. .cate-count {
  10656. background: #fff;
  10657. }
  10658. }
  10659. }
  10660. }
  10661. .modal-mid {
  10662. background: #fff;
  10663. padding: 10px;
  10664. display: flex;
  10665. flex-direction: column;
  10666. .tab-row {
  10667. display: flex;
  10668. gap: 8px;
  10669. margin-bottom: 0;
  10670. .tab {
  10671. padding: 5px 12px;
  10672. border: 1px solid #e5e7eb;
  10673. border-radius: 999px;
  10674. background: #fff;
  10675. color: #475569;
  10676. font-size: 12px;
  10677. cursor: pointer;
  10678. transition: all 0.2s ease;
  10679. &.active {
  10680. border-color: #c8d7fb;
  10681. background: #eef4ff;
  10682. color: #3651d9;
  10683. }
  10684. }
  10685. }
  10686. .filters {
  10687. display: flex;
  10688. gap: 10px;
  10689. margin-bottom: 8px;
  10690. padding: 8px 10px;
  10691. border: 1px solid #e8edf5;
  10692. border-radius: 12px;
  10693. background: #f8fafc;
  10694. .filter-item {
  10695. display: flex;
  10696. flex-direction: column;
  10697. gap: 6px;
  10698. min-width: 160px;
  10699. &--grow {
  10700. flex: 1;
  10701. }
  10702. }
  10703. .filter-label {
  10704. color: #64748b;
  10705. font-size: 12px;
  10706. font-weight: 600;
  10707. line-height: 1;
  10708. }
  10709. .source-select {
  10710. width: 160px;
  10711. }
  10712. }
  10713. .list-summary {
  10714. display: flex;
  10715. justify-content: space-between;
  10716. gap: 12px;
  10717. margin-bottom: 10px;
  10718. color: #6b7280;
  10719. font-size: 12px;
  10720. line-height: 1.6;
  10721. }
  10722. .tpl-list {
  10723. flex: 1;
  10724. overflow: auto;
  10725. padding-right: 2px;
  10726. }
  10727. .tpl-item {
  10728. min-height: 52px;
  10729. padding: 7px 10px;
  10730. border-radius: 12px;
  10731. background: #fff;
  10732. margin-bottom: 6px;
  10733. display: flex;
  10734. align-items: center;
  10735. cursor: pointer;
  10736. border: 1px solid #e5e7eb;
  10737. position: relative;
  10738. transition: all 0.2s ease;
  10739. &:hover {
  10740. border-color: #dbe7ff;
  10741. background: #f8fbff;
  10742. }
  10743. &.active {
  10744. background: #eef4ff;
  10745. border-color: #c8d7fb;
  10746. }
  10747. .tpl-main {
  10748. display: flex;
  10749. align-items: center;
  10750. flex-wrap: wrap;
  10751. gap: 8px 12px;
  10752. min-width: 0;
  10753. flex: 1;
  10754. }
  10755. .name {
  10756. overflow: hidden;
  10757. text-overflow: ellipsis;
  10758. white-space: nowrap;
  10759. color: #111827;
  10760. font-size: 14px;
  10761. font-weight: 600;
  10762. line-height: 1.4;
  10763. }
  10764. .tpl-meta {
  10765. display: flex;
  10766. flex-wrap: wrap;
  10767. gap: 4px 12px;
  10768. min-width: 0;
  10769. }
  10770. .tpl-meta-text {
  10771. color: #64748b;
  10772. font-size: 12px;
  10773. line-height: 1.4;
  10774. white-space: nowrap;
  10775. }
  10776. .selected-mark {
  10777. flex-shrink: 0;
  10778. margin-left: 12px;
  10779. padding: 3px 10px;
  10780. border-radius: 999px;
  10781. border: 1px solid #c8d7fb;
  10782. background: #fff;
  10783. color: #3651d9;
  10784. font-size: 12px;
  10785. line-height: 18px;
  10786. font-weight: 600;
  10787. }
  10788. }
  10789. .empty {
  10790. text-align: center;
  10791. color: #94a3b8;
  10792. margin-top: 40px;
  10793. }
  10794. }
  10795. .modal-right {
  10796. padding: 10px;
  10797. display: flex;
  10798. flex-direction: column;
  10799. .preview-overview {
  10800. display: grid;
  10801. grid-template-columns: repeat(2, minmax(0, 1fr));
  10802. gap: 8px;
  10803. margin-bottom: 6px;
  10804. }
  10805. .overview-card {
  10806. padding: 8px 10px;
  10807. border: 1px solid #e8edf5;
  10808. border-radius: 12px;
  10809. background: #f8fafc;
  10810. display: flex;
  10811. flex-direction: column;
  10812. gap: 4px;
  10813. }
  10814. .overview-label {
  10815. color: #6b7280;
  10816. font-size: 12px;
  10817. line-height: 1;
  10818. }
  10819. .overview-value {
  10820. color: #111827;
  10821. font-size: 14px;
  10822. font-weight: 600;
  10823. line-height: 1.5;
  10824. word-break: break-word;
  10825. }
  10826. .preview-meta {
  10827. margin-bottom: 6px;
  10828. padding: 8px 10px;
  10829. border: 1px solid #e8edf5;
  10830. border-radius: 12px;
  10831. background: #fff;
  10832. }
  10833. .preview-meta-row {
  10834. display: flex;
  10835. gap: 12px;
  10836. font-size: 13px;
  10837. line-height: 1.6;
  10838. margin-bottom: 0;
  10839. &.preview-meta-row--headers {
  10840. align-items: flex-start;
  10841. }
  10842. &:last-child {
  10843. margin-bottom: 0;
  10844. }
  10845. }
  10846. .meta-label {
  10847. flex: 0 0 64px;
  10848. color: #6b7280;
  10849. }
  10850. .meta-value {
  10851. min-width: 0;
  10852. flex: 1;
  10853. color: #334155;
  10854. word-break: break-word;
  10855. &.meta-value--scroll {
  10856. max-height: 88px;
  10857. padding-right: 6px;
  10858. overflow-y: auto;
  10859. overflow-x: hidden;
  10860. }
  10861. }
  10862. .preview-empty {
  10863. margin-bottom: 8px;
  10864. padding: 14px 12px;
  10865. border: 1px dashed #dbe2f0;
  10866. border-radius: 12px;
  10867. background: #f8fbff;
  10868. }
  10869. .empty-title {
  10870. color: #111827;
  10871. font-size: 14px;
  10872. font-weight: 600;
  10873. }
  10874. .empty-desc {
  10875. margin-top: 6px;
  10876. color: #6b7280;
  10877. font-size: 12px;
  10878. line-height: 1.7;
  10879. }
  10880. .preview-rule-title {
  10881. margin-bottom: 4px;
  10882. font-size: 14px;
  10883. font-weight: 600;
  10884. color: #111827;
  10885. }
  10886. .preview-rule-table {
  10887. flex: 1;
  10888. min-height: 0;
  10889. border: 1px solid #e5e7eb;
  10890. border-radius: 12px;
  10891. overflow: auto;
  10892. background: #fff;
  10893. .rule-head,
  10894. .rule-row {
  10895. display: grid;
  10896. grid-template-columns: 1fr 1fr;
  10897. }
  10898. .rule-head {
  10899. background: #f3f4f6;
  10900. font-weight: 600;
  10901. color: #334155;
  10902. }
  10903. .rule-row {
  10904. border-top: 1px solid #f1f5f9;
  10905. color: #475569;
  10906. }
  10907. .rule-head > span,
  10908. .rule-row > .rule-cell {
  10909. padding: 8px 10px;
  10910. font-size: 12px;
  10911. overflow: hidden;
  10912. text-overflow: ellipsis;
  10913. white-space: nowrap;
  10914. }
  10915. .rule-cell {
  10916. min-width: 0;
  10917. &.rule-cell--target {
  10918. display: flex;
  10919. align-items: center;
  10920. gap: 8px;
  10921. }
  10922. }
  10923. .rule-target-label {
  10924. min-width: 0;
  10925. display: inline-flex;
  10926. align-items: center;
  10927. color: #24324b;
  10928. }
  10929. .rule-required-mark {
  10930. flex: 0 0 auto;
  10931. margin-right: 4px;
  10932. color: #ef4444;
  10933. font-weight: 600;
  10934. }
  10935. .rule-target-text {
  10936. min-width: 0;
  10937. overflow: hidden;
  10938. text-overflow: ellipsis;
  10939. white-space: nowrap;
  10940. }
  10941. .rule-required-badge {
  10942. flex: 0 0 auto;
  10943. padding: 0 6px;
  10944. border-radius: 999px;
  10945. background: #fef2f2;
  10946. color: #dc2626;
  10947. font-size: 11px;
  10948. line-height: 18px;
  10949. font-weight: 500;
  10950. }
  10951. }
  10952. .empty {
  10953. color: #94a3b8;
  10954. }
  10955. .modal-actions {
  10956. margin-top: auto;
  10957. display: flex;
  10958. justify-content: flex-end;
  10959. gap: 10px;
  10960. padding-top: 8px;
  10961. border-top: 1px solid #eef2f7;
  10962. .ant-btn {
  10963. min-width: 76px;
  10964. height: 36px;
  10965. padding: 0 18px;
  10966. border-radius: 18px;
  10967. border-width: 1px;
  10968. font-size: 13px;
  10969. font-weight: 500;
  10970. box-shadow: none;
  10971. }
  10972. }
  10973. }
  10974. @media (max-width: 1260px) {
  10975. .template-modal-layout {
  10976. grid-template-columns: 220px minmax(0, 1fr);
  10977. grid-template-rows: minmax(0, 1fr) 320px;
  10978. height: auto;
  10979. min-height: 640px;
  10980. }
  10981. .modal-right {
  10982. grid-column: 1 / span 2;
  10983. }
  10984. .preview-rule-table {
  10985. min-height: 180px;
  10986. }
  10987. }
  10988. @media (max-width: 900px) {
  10989. .template-modal-layout {
  10990. grid-template-columns: 1fr;
  10991. grid-template-rows: auto auto auto;
  10992. height: auto;
  10993. }
  10994. .modal-right {
  10995. grid-column: auto;
  10996. }
  10997. .filters,
  10998. .list-summary {
  10999. flex-direction: column;
  11000. }
  11001. .preview-overview {
  11002. grid-template-columns: 1fr;
  11003. }
  11004. }
  11005. }
  11006. @keyframes cleaning-sheet-status-spin {
  11007. from {
  11008. transform: rotate(0deg);
  11009. }
  11010. to {
  11011. transform: rotate(360deg);
  11012. }
  11013. }
  11014. </style>